@decantr/verifier 1.1.0 → 2.0.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 +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +338 -57
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/schema/project-audit-report.v1.json +1 -4
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ function isBlocking(report: ProjectHealthReport) {
|
|
|
51
51
|
|
|
52
52
|
## Compatibility
|
|
53
53
|
|
|
54
|
-
`@decantr/verifier` is stable in the `
|
|
54
|
+
`@decantr/verifier` is stable in the `2.x` line for the documented verifier APIs and published report-schema exports.
|
|
55
55
|
|
|
56
56
|
- new checks and additive report fields may appear in compatible releases
|
|
57
57
|
- breaking report-shape or exported API changes require a major version
|
package/dist/index.d.ts
CHANGED
|
@@ -54,7 +54,7 @@ declare function emptyRuntimeAudit(failures?: string[]): RuntimeAudit;
|
|
|
54
54
|
declare function auditBuiltDist(projectRoot: string, options?: BuiltDistAuditOptions): Promise<RuntimeAudit>;
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
57
|
+
* Experiential interaction verifier.
|
|
58
58
|
*
|
|
59
59
|
* Scans generated source files for evidence that declared `interactions[]`
|
|
60
60
|
* (from pattern.v2.json) are actually implemented. This is the enforcement
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { existsSync as existsSync2,
|
|
2
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync } from "fs";
|
|
3
3
|
import { readFile } from "fs/promises";
|
|
4
|
-
import { extname as extname2, isAbsolute, join as join2, relative, resolve } from "path";
|
|
5
|
-
import { evaluateGuard,
|
|
4
|
+
import { dirname, extname as extname2, isAbsolute, join as join2, relative, resolve } from "path";
|
|
5
|
+
import { evaluateGuard, isV4, validateEssence } from "@decantr/essence-spec";
|
|
6
6
|
import * as ts from "typescript";
|
|
7
7
|
|
|
8
8
|
// src/runtime.ts
|
|
9
|
-
import { existsSync, readFileSync } from "fs";
|
|
9
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
10
10
|
import { createServer } from "http";
|
|
11
11
|
import { extname, join, normalize } from "path";
|
|
12
12
|
var CONTENT_TYPES = {
|
|
@@ -224,6 +224,122 @@ function countSecretLeakSignals(js) {
|
|
|
224
224
|
];
|
|
225
225
|
return patterns.reduce((count, pattern) => count + (js.match(pattern)?.length ?? 0), 0);
|
|
226
226
|
}
|
|
227
|
+
function collectFiles(rootDir, predicate) {
|
|
228
|
+
if (!existsSync(rootDir)) return [];
|
|
229
|
+
const files = [];
|
|
230
|
+
const walk = (dir) => {
|
|
231
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
232
|
+
const path = join(dir, entry.name);
|
|
233
|
+
if (entry.isDirectory()) {
|
|
234
|
+
walk(path);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (entry.isFile() && predicate(path)) {
|
|
238
|
+
files.push(path);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
walk(rootDir);
|
|
243
|
+
return files.sort();
|
|
244
|
+
}
|
|
245
|
+
function auditNextBuildOutput(projectRoot) {
|
|
246
|
+
const nextDir = join(projectRoot, ".next");
|
|
247
|
+
const buildIdPresent = existsSync(join(nextDir, "BUILD_ID"));
|
|
248
|
+
const buildManifestPresent = existsSync(join(nextDir, "build-manifest.json"));
|
|
249
|
+
const appServerDir = join(nextDir, "server", "app");
|
|
250
|
+
const htmlFiles = collectFiles(appServerDir, (filePath) => {
|
|
251
|
+
const relativePath = filePath.slice(appServerDir.length + 1).replace(/\\/g, "/");
|
|
252
|
+
return filePath.endsWith(".html") && !relativePath.startsWith("_");
|
|
253
|
+
});
|
|
254
|
+
const htmlDocuments = htmlFiles.map((filePath) => readFileSync(filePath, "utf-8"));
|
|
255
|
+
const staticAssetFiles = collectFiles(
|
|
256
|
+
join(nextDir, "static"),
|
|
257
|
+
(filePath) => /\.(?:js|css|json|svg|png|jpe?g|webp)$/i.test(filePath)
|
|
258
|
+
);
|
|
259
|
+
let totalAssetBytes = 0;
|
|
260
|
+
let jsAssetBytes = 0;
|
|
261
|
+
let cssAssetBytes = 0;
|
|
262
|
+
let largestAssetPath = null;
|
|
263
|
+
let largestAssetBytes = 0;
|
|
264
|
+
let combinedJs = "";
|
|
265
|
+
for (const assetFile of staticAssetFiles) {
|
|
266
|
+
const byteLength = statSync(assetFile).size;
|
|
267
|
+
const assetPath = `/_next/static/${assetFile.slice(join(nextDir, "static").length + 1).replace(/\\/g, "/")}`;
|
|
268
|
+
totalAssetBytes += byteLength;
|
|
269
|
+
if (assetFile.endsWith(".js")) {
|
|
270
|
+
jsAssetBytes += byteLength;
|
|
271
|
+
combinedJs += readFileSync(assetFile, "utf-8");
|
|
272
|
+
}
|
|
273
|
+
if (assetFile.endsWith(".css")) {
|
|
274
|
+
cssAssetBytes += byteLength;
|
|
275
|
+
}
|
|
276
|
+
if (byteLength > largestAssetBytes) {
|
|
277
|
+
largestAssetBytes = byteLength;
|
|
278
|
+
largestAssetPath = assetPath;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const hasHtmlDocuments = htmlDocuments.length > 0;
|
|
282
|
+
const titleOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<title>[^<]+<\/title>/i.test(html));
|
|
283
|
+
const langOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<html[^>]*\slang=(["'])[^"']+\1/i.test(html));
|
|
284
|
+
const viewportOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<meta[^>]+name=(["'])viewport\1[^>]*>/i.test(html));
|
|
285
|
+
const charsetOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<meta[^>]+charset=/i.test(html));
|
|
286
|
+
const failures = ["next-build-output"];
|
|
287
|
+
if (!buildIdPresent) {
|
|
288
|
+
failures.push("next-build-id-missing");
|
|
289
|
+
}
|
|
290
|
+
if (!buildManifestPresent) {
|
|
291
|
+
failures.push("next-build-manifest-missing");
|
|
292
|
+
}
|
|
293
|
+
if (staticAssetFiles.length === 0) {
|
|
294
|
+
failures.push("next-assets-missing");
|
|
295
|
+
}
|
|
296
|
+
const passed = buildIdPresent && buildManifestPresent && staticAssetFiles.length > 0;
|
|
297
|
+
return {
|
|
298
|
+
distPresent: true,
|
|
299
|
+
indexPresent: true,
|
|
300
|
+
checked: true,
|
|
301
|
+
passed,
|
|
302
|
+
rootDocumentOk: hasHtmlDocuments || passed,
|
|
303
|
+
titleOk,
|
|
304
|
+
langOk,
|
|
305
|
+
viewportOk,
|
|
306
|
+
charsetOk,
|
|
307
|
+
cspSignalOk: true,
|
|
308
|
+
inlineScriptCount: 0,
|
|
309
|
+
inlineEventHandlerCount: 0,
|
|
310
|
+
externalScriptsWithoutIntegrityCount: 0,
|
|
311
|
+
externalScriptsWithIntegrityMissingCrossoriginCount: 0,
|
|
312
|
+
externalStylesheetsWithoutIntegrityCount: 0,
|
|
313
|
+
externalStylesheetsWithIntegrityMissingCrossoriginCount: 0,
|
|
314
|
+
externalScriptsWithInsecureTransportCount: 0,
|
|
315
|
+
externalStylesheetsWithInsecureTransportCount: 0,
|
|
316
|
+
externalMediaSourcesWithInsecureTransportCount: 0,
|
|
317
|
+
externalBlankLinksWithoutRelCount: 0,
|
|
318
|
+
externalIframesWithoutSandboxCount: 0,
|
|
319
|
+
externalIframesWithInsecureTransportCount: 0,
|
|
320
|
+
jsEvalSignalCount: countDynamicCodeSignals(combinedJs),
|
|
321
|
+
jsHtmlInjectionSignalCount: countHtmlInjectionSignals(combinedJs),
|
|
322
|
+
jsInsecureTransportSignalCount: countInsecureTransportSignals(combinedJs),
|
|
323
|
+
jsSecretSignalCount: countSecretLeakSignals(combinedJs),
|
|
324
|
+
assetCount: staticAssetFiles.length,
|
|
325
|
+
assetsPassed: staticAssetFiles.length,
|
|
326
|
+
routeHintsChecked: [],
|
|
327
|
+
routeHintsMatched: 0,
|
|
328
|
+
routeHintsCoverageOk: true,
|
|
329
|
+
routeDocumentsChecked: 0,
|
|
330
|
+
routeDocumentsPassed: 0,
|
|
331
|
+
routeDocumentsHardenedCount: 0,
|
|
332
|
+
routeDocumentsCoverageOk: true,
|
|
333
|
+
routeDocumentsHardeningOk: true,
|
|
334
|
+
fullRouteCoverageOk: true,
|
|
335
|
+
totalAssetBytes,
|
|
336
|
+
jsAssetBytes,
|
|
337
|
+
cssAssetBytes,
|
|
338
|
+
largestAssetPath,
|
|
339
|
+
largestAssetBytes,
|
|
340
|
+
failures
|
|
341
|
+
};
|
|
342
|
+
}
|
|
227
343
|
function normalizeRouteHint(route) {
|
|
228
344
|
if (!route || route === "/") return "/";
|
|
229
345
|
const dynamicIndex = route.indexOf("/:");
|
|
@@ -280,6 +396,9 @@ async function startStaticServer(rootDir) {
|
|
|
280
396
|
async function auditBuiltDist(projectRoot, options = {}) {
|
|
281
397
|
const distDir = options.distDir ?? join(projectRoot, "dist");
|
|
282
398
|
if (!existsSync(distDir)) {
|
|
399
|
+
if (!options.distDir && existsSync(join(projectRoot, ".next"))) {
|
|
400
|
+
return auditNextBuildOutput(projectRoot);
|
|
401
|
+
}
|
|
283
402
|
return emptyRuntimeAudit(["dist-missing"]);
|
|
284
403
|
}
|
|
285
404
|
const indexPath = join(distDir, "index.html");
|
|
@@ -686,6 +805,54 @@ function recordSourceAudit(bucket, filePath, count) {
|
|
|
686
805
|
function sourceAuditBucketsOverlap(a, b) {
|
|
687
806
|
return a.files.some((file) => b.files.includes(file));
|
|
688
807
|
}
|
|
808
|
+
function normalizeSourceAuditPath(filePath) {
|
|
809
|
+
return filePath.replace(/\\/g, "/");
|
|
810
|
+
}
|
|
811
|
+
function getNextAppLayoutGuardRoot(filePath) {
|
|
812
|
+
const normalized = normalizeSourceAuditPath(filePath);
|
|
813
|
+
const match = normalized.match(
|
|
814
|
+
/^(?:src\/)?app\/(?:\([^/]+\)\/)*([^/.()[\]]+)\/layout\.[cm]?[jt]sx?$/i
|
|
815
|
+
);
|
|
816
|
+
return match?.[1] ?? null;
|
|
817
|
+
}
|
|
818
|
+
function getNextAppRouteRoot(filePath) {
|
|
819
|
+
const normalized = normalizeSourceAuditPath(filePath);
|
|
820
|
+
const match = normalized.match(/^(?:src\/)?app\/(?:\([^/]+\)\/)*([^/.()[\]]+)(?:\/|$)/i);
|
|
821
|
+
return match?.[1] ?? null;
|
|
822
|
+
}
|
|
823
|
+
function getRouteRoot(route) {
|
|
824
|
+
const firstSegment = route.replace(/^\/+/, "").split("/")[0];
|
|
825
|
+
if (!firstSegment || firstSegment.startsWith(":") || firstSegment.startsWith("[")) {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
return firstSegment;
|
|
829
|
+
}
|
|
830
|
+
function isNextAppPublicChromeFile(filePath) {
|
|
831
|
+
const normalized = normalizeSourceAuditPath(filePath);
|
|
832
|
+
return /^(?:src\/)?app\/(?:\([^/]+\)\/)*(?:layout|nav|nav-header|navigation|header|footer|sidebar)\.[cm]?[jt]sx?$/i.test(
|
|
833
|
+
normalized
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
function sourceAuditProtectedSurfacesCoveredByGuardedNextLayouts(protectedSurfaces, authGuards, topology) {
|
|
837
|
+
const guardedRoots = /* @__PURE__ */ new Set();
|
|
838
|
+
for (const file of authGuards.files) {
|
|
839
|
+
const root = getNextAppLayoutGuardRoot(file);
|
|
840
|
+
if (root) {
|
|
841
|
+
guardedRoots.add(root);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (guardedRoots.size === 0 || protectedSurfaces.files.length === 0) {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
const guardedPrimaryRouteRoots = topology.primaryRoutes.map(getRouteRoot).filter((root) => Boolean(root)).filter((root) => guardedRoots.has(root));
|
|
848
|
+
return protectedSurfaces.files.every((file) => {
|
|
849
|
+
const appRoot = getNextAppRouteRoot(file);
|
|
850
|
+
if (appRoot && guardedRoots.has(appRoot)) {
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
return isNextAppPublicChromeFile(file) && guardedPrimaryRouteRoots.length > 0;
|
|
854
|
+
});
|
|
855
|
+
}
|
|
689
856
|
function isAuditableSourceFile(filePath) {
|
|
690
857
|
if (/\.d\.ts$/i.test(filePath)) return false;
|
|
691
858
|
return /\.(?:[cm]?[jt]sx?)$/i.test(filePath);
|
|
@@ -726,7 +893,7 @@ function collectProjectSourceFiles(projectRoot) {
|
|
|
726
893
|
"coverage"
|
|
727
894
|
]);
|
|
728
895
|
const walk = (dir) => {
|
|
729
|
-
for (const entry of
|
|
896
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
730
897
|
if (ignoredDirNames.has(entry.name)) continue;
|
|
731
898
|
const absolutePath = join2(dir, entry.name);
|
|
732
899
|
if (entry.isDirectory()) {
|
|
@@ -770,7 +937,7 @@ function collectProjectStyleFiles(projectRoot) {
|
|
|
770
937
|
"coverage"
|
|
771
938
|
]);
|
|
772
939
|
const walk = (dir) => {
|
|
773
|
-
for (const entry of
|
|
940
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
774
941
|
if (ignoredDirNames.has(entry.name)) continue;
|
|
775
942
|
const absolutePath = join2(dir, entry.name);
|
|
776
943
|
if (entry.isDirectory()) {
|
|
@@ -799,8 +966,126 @@ function countReducedMotionSignals(code) {
|
|
|
799
966
|
];
|
|
800
967
|
return patterns.reduce((count, pattern) => count + (pattern.test(code) ? 1 : 0), 0);
|
|
801
968
|
}
|
|
969
|
+
function hasModuleDirective(filePath, code, directive) {
|
|
970
|
+
const sourceFile = ts.createSourceFile(
|
|
971
|
+
filePath,
|
|
972
|
+
code,
|
|
973
|
+
ts.ScriptTarget.Latest,
|
|
974
|
+
true,
|
|
975
|
+
getScriptKind(filePath)
|
|
976
|
+
);
|
|
977
|
+
for (const statement of sourceFile.statements) {
|
|
978
|
+
if (ts.isExpressionStatement(statement) && ts.isStringLiteralLike(statement.expression)) {
|
|
979
|
+
if (statement.expression.text === directive) {
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
function importDeclarationHasRuntimeBinding(statement) {
|
|
989
|
+
const clause = statement.importClause;
|
|
990
|
+
if (!clause) return true;
|
|
991
|
+
if (clause.isTypeOnly) return false;
|
|
992
|
+
if (clause.name) return true;
|
|
993
|
+
if (!clause.namedBindings) return true;
|
|
994
|
+
if (ts.isNamespaceImport(clause.namedBindings)) return true;
|
|
995
|
+
return clause.namedBindings.elements.some((element) => !element.isTypeOnly);
|
|
996
|
+
}
|
|
997
|
+
function collectRuntimeImportSpecifiers(entry) {
|
|
998
|
+
const sourceFile = ts.createSourceFile(
|
|
999
|
+
entry.relativePath,
|
|
1000
|
+
entry.code,
|
|
1001
|
+
ts.ScriptTarget.Latest,
|
|
1002
|
+
true,
|
|
1003
|
+
getScriptKind(entry.relativePath)
|
|
1004
|
+
);
|
|
1005
|
+
const specifiers = [];
|
|
1006
|
+
for (const statement of sourceFile.statements) {
|
|
1007
|
+
if (ts.isImportDeclaration(statement) && ts.isStringLiteralLike(statement.moduleSpecifier) && importDeclarationHasRuntimeBinding(statement)) {
|
|
1008
|
+
specifiers.push(statement.moduleSpecifier.text);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return specifiers;
|
|
1012
|
+
}
|
|
1013
|
+
function resolveSourceImportTarget(projectRoot, sourceAbsolutePath, specifier) {
|
|
1014
|
+
let basePath = null;
|
|
1015
|
+
if (specifier.startsWith("@/")) {
|
|
1016
|
+
basePath = join2(projectRoot, "src", specifier.slice(2));
|
|
1017
|
+
} else if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
|
1018
|
+
basePath = resolve(dirname(sourceAbsolutePath), specifier);
|
|
1019
|
+
}
|
|
1020
|
+
if (!basePath) return null;
|
|
1021
|
+
const candidates = [
|
|
1022
|
+
basePath,
|
|
1023
|
+
`${basePath}.ts`,
|
|
1024
|
+
`${basePath}.tsx`,
|
|
1025
|
+
`${basePath}.js`,
|
|
1026
|
+
`${basePath}.jsx`,
|
|
1027
|
+
`${basePath}.mts`,
|
|
1028
|
+
`${basePath}.cts`,
|
|
1029
|
+
join2(basePath, "index.ts"),
|
|
1030
|
+
join2(basePath, "index.tsx"),
|
|
1031
|
+
join2(basePath, "index.js"),
|
|
1032
|
+
join2(basePath, "index.jsx")
|
|
1033
|
+
];
|
|
1034
|
+
for (const candidate of candidates) {
|
|
1035
|
+
if (existsSync2(candidate) && isAuditableSourceFile(candidate)) {
|
|
1036
|
+
return relative(projectRoot, candidate) || candidate;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
function collectClientReachableSourceFiles(projectRoot, entries) {
|
|
1042
|
+
const entriesByRelativePath = new Map(entries.map((entry) => [entry.relativePath, entry]));
|
|
1043
|
+
const runtimeImportGraph = /* @__PURE__ */ new Map();
|
|
1044
|
+
for (const entry of entries) {
|
|
1045
|
+
const targets = collectRuntimeImportSpecifiers(entry).map((specifier) => resolveSourceImportTarget(projectRoot, entry.absolutePath, specifier)).filter((target) => Boolean(target));
|
|
1046
|
+
runtimeImportGraph.set(entry.relativePath, targets);
|
|
1047
|
+
}
|
|
1048
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
1049
|
+
const queue = entries.filter((entry) => hasModuleDirective(entry.relativePath, entry.code, "use client")).map((entry) => entry.relativePath);
|
|
1050
|
+
for (const root of queue) {
|
|
1051
|
+
reachable.add(root);
|
|
1052
|
+
}
|
|
1053
|
+
while (queue.length > 0) {
|
|
1054
|
+
const current = queue.shift();
|
|
1055
|
+
for (const target of runtimeImportGraph.get(current) ?? []) {
|
|
1056
|
+
const targetEntry = entriesByRelativePath.get(target);
|
|
1057
|
+
if (targetEntry && hasModuleDirective(target, targetEntry.code, "use server")) {
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
if (!reachable.has(target)) {
|
|
1061
|
+
reachable.add(target);
|
|
1062
|
+
queue.push(target);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return reachable;
|
|
1067
|
+
}
|
|
1068
|
+
function isClientAuthHeaderSource(relativePath, code, clientReachableFiles) {
|
|
1069
|
+
if (hasModuleDirective(relativePath, code, "use server")) {
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
if (clientReachableFiles.has(relativePath)) {
|
|
1073
|
+
return true;
|
|
1074
|
+
}
|
|
1075
|
+
const normalized = normalizeSourceAuditPath(relativePath);
|
|
1076
|
+
if (/^(?:src\/)?app\//i.test(normalized)) {
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
return /(?:^|\/)(?:components|routes|pages|hooks|providers)\//i.test(normalized);
|
|
1080
|
+
}
|
|
802
1081
|
function auditProjectSourceTree(projectRoot) {
|
|
803
1082
|
const sourceFiles = collectProjectSourceFiles(projectRoot);
|
|
1083
|
+
const sourceEntries = sourceFiles.map((sourceFile) => ({
|
|
1084
|
+
absolutePath: sourceFile,
|
|
1085
|
+
relativePath: relative(projectRoot, sourceFile) || sourceFile,
|
|
1086
|
+
code: readFileSync2(sourceFile, "utf-8")
|
|
1087
|
+
}));
|
|
1088
|
+
const clientReachableFiles = collectClientReachableSourceFiles(projectRoot, sourceEntries);
|
|
804
1089
|
const summary = {
|
|
805
1090
|
filesChecked: sourceFiles.length,
|
|
806
1091
|
inlineStyles: createSourceAuditBucket(),
|
|
@@ -869,10 +1154,13 @@ function auditProjectSourceTree(projectRoot) {
|
|
|
869
1154
|
interactionSafetyIssues: createSourceAuditBucket(),
|
|
870
1155
|
authInputHintIssues: createSourceAuditBucket()
|
|
871
1156
|
};
|
|
872
|
-
for (const
|
|
873
|
-
const relativePath = relative(projectRoot, sourceFile) || sourceFile;
|
|
874
|
-
const code = readFileSync2(sourceFile, "utf-8");
|
|
1157
|
+
for (const { relativePath, code } of sourceEntries) {
|
|
875
1158
|
const signals = analyzeAstSignals(relativePath, code);
|
|
1159
|
+
const isClientAuthHeaderSourceFile = isClientAuthHeaderSource(
|
|
1160
|
+
relativePath,
|
|
1161
|
+
code,
|
|
1162
|
+
clientReachableFiles
|
|
1163
|
+
);
|
|
876
1164
|
const accessibilityIssueCount = signals.iconOnlyButtonWithoutLabelCount + signals.iconOnlyLinkWithoutLabelCount + signals.clickableNonSemanticCount + signals.unlabeledNavigationLandmarkCount + signals.imageWithoutAltCount + signals.iframeWithoutTitleCount + signals.dialogWithoutLabelCount + signals.dialogWithoutModalHintCount + signals.tableWithoutHeaderCount + signals.tableWithoutCaptionCount + signals.formControlWithoutLabelCount;
|
|
877
1165
|
const securityRiskPatternCount = signals.dangerousHtmlCount + signals.rawHtmlInjectionCount + signals.dynamicEvalCount + signals.hardcodedSecretSignalCount + signals.clientSecretEnvReferenceCount + signals.localhostEndpointCount + signals.wildcardPostMessageCount + signals.windowOpenWithoutNoopenerCount + signals.externalIframeWithoutSandboxCount + signals.insecureExternalIframeCount + signals.insecureFormActionCount + signals.insecureAuthFormMethodCount + signals.insecureTransportEndpointCount + signals.insecureExternalImageCount + signals.authCookieMissingHardeningCount + signals.authOpenRedirectSignalCount + signals.authExternalRedirectSignalCount + signals.authProviderStateMissingCount + signals.authProviderPkceMissingCount + signals.authProviderNonceMissingCount + signals.externalBlankLinkWithoutRelCount;
|
|
878
1166
|
const authInputHintIssueCount = signals.emailAutocompleteMissingCount + signals.passwordAutocompleteMissingCount + signals.otpAutocompleteMissingCount + signals.authAutocompleteDisabledCount + signals.authAutocompleteSemanticMismatchCount + signals.authInputTypeMismatchCount;
|
|
@@ -1042,8 +1330,16 @@ function auditProjectSourceTree(projectRoot) {
|
|
|
1042
1330
|
recordSourceAudit(summary.authStorageClears, relativePath, signals.authStorageClearCount);
|
|
1043
1331
|
recordSourceAudit(summary.authCookieWrites, relativePath, signals.authCookieWriteCount);
|
|
1044
1332
|
recordSourceAudit(summary.authCookieClears, relativePath, signals.authCookieClearCount);
|
|
1045
|
-
recordSourceAudit(
|
|
1046
|
-
|
|
1333
|
+
recordSourceAudit(
|
|
1334
|
+
summary.authHeaderWrites,
|
|
1335
|
+
relativePath,
|
|
1336
|
+
isClientAuthHeaderSourceFile ? signals.authHeaderWriteCount : 0
|
|
1337
|
+
);
|
|
1338
|
+
recordSourceAudit(
|
|
1339
|
+
summary.authHeaderClears,
|
|
1340
|
+
relativePath,
|
|
1341
|
+
isClientAuthHeaderSourceFile ? signals.authHeaderClearCount : 0
|
|
1342
|
+
);
|
|
1047
1343
|
recordSourceAudit(summary.authCacheClients, relativePath, signals.authCacheClientCount);
|
|
1048
1344
|
recordSourceAudit(summary.authCacheClears, relativePath, signals.authCacheClearCount);
|
|
1049
1345
|
recordSourceAudit(summary.authRefreshSignals, relativePath, signals.authRefreshSignalCount);
|
|
@@ -1115,7 +1411,7 @@ function buildRegistryContext(projectRoot) {
|
|
|
1115
1411
|
const cachedThemesDir = join2(cacheDir, "@official", "themes");
|
|
1116
1412
|
try {
|
|
1117
1413
|
if (existsSync2(cachedThemesDir)) {
|
|
1118
|
-
for (const file of
|
|
1414
|
+
for (const file of readdirSync2(cachedThemesDir).filter(
|
|
1119
1415
|
(name) => name.endsWith(".json") && name !== "index.json"
|
|
1120
1416
|
)) {
|
|
1121
1417
|
const data = JSON.parse(readFileSync2(join2(cachedThemesDir, file), "utf-8"));
|
|
@@ -1129,7 +1425,7 @@ function buildRegistryContext(projectRoot) {
|
|
|
1129
1425
|
const customThemesDir = join2(customDir, "themes");
|
|
1130
1426
|
try {
|
|
1131
1427
|
if (existsSync2(customThemesDir)) {
|
|
1132
|
-
for (const file of
|
|
1428
|
+
for (const file of readdirSync2(customThemesDir).filter((name) => name.endsWith(".json"))) {
|
|
1133
1429
|
const data = JSON.parse(readFileSync2(join2(customThemesDir, file), "utf-8"));
|
|
1134
1430
|
if (data.id) {
|
|
1135
1431
|
themeRegistry.set(`custom:${data.id}`, { modes: data.modes || ["light", "dark"] });
|
|
@@ -1141,7 +1437,7 @@ function buildRegistryContext(projectRoot) {
|
|
|
1141
1437
|
const cachedPatternsDir = join2(cacheDir, "@official", "patterns");
|
|
1142
1438
|
try {
|
|
1143
1439
|
if (existsSync2(cachedPatternsDir)) {
|
|
1144
|
-
for (const file of
|
|
1440
|
+
for (const file of readdirSync2(cachedPatternsDir).filter(
|
|
1145
1441
|
(name) => name.endsWith(".json") && name !== "index.json"
|
|
1146
1442
|
)) {
|
|
1147
1443
|
const data = JSON.parse(readFileSync2(join2(cachedPatternsDir, file), "utf-8"));
|
|
@@ -1167,17 +1463,7 @@ function guardViolationToFinding(violation) {
|
|
|
1167
1463
|
}
|
|
1168
1464
|
function countPages(essence) {
|
|
1169
1465
|
if (!essence) return 0;
|
|
1170
|
-
|
|
1171
|
-
const v3 = essence;
|
|
1172
|
-
if (v3.blueprint.sections?.length) {
|
|
1173
|
-
return v3.blueprint.sections.reduce((sum, section) => sum + section.pages.length, 0);
|
|
1174
|
-
}
|
|
1175
|
-
return v3.blueprint.pages?.length ?? 0;
|
|
1176
|
-
}
|
|
1177
|
-
if ("structure" in essence && Array.isArray(essence.structure)) {
|
|
1178
|
-
return essence.structure.length;
|
|
1179
|
-
}
|
|
1180
|
-
return 0;
|
|
1466
|
+
return essence.blueprint.sections.reduce((sum, section) => sum + section.pages.length, 0);
|
|
1181
1467
|
}
|
|
1182
1468
|
function makeFinding(input) {
|
|
1183
1469
|
return input;
|
|
@@ -1252,31 +1538,19 @@ function extractRouteHintsFromEssence(essence) {
|
|
|
1252
1538
|
return ["/"];
|
|
1253
1539
|
}
|
|
1254
1540
|
const routes = /* @__PURE__ */ new Set(["/"]);
|
|
1255
|
-
if (
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1258
|
-
for (const page of section.pages ?? []) {
|
|
1541
|
+
if (isV4(essence)) {
|
|
1542
|
+
for (const section of essence.blueprint.sections) {
|
|
1543
|
+
for (const page of section.pages) {
|
|
1259
1544
|
if (typeof page.route === "string" && page.route.length > 0) {
|
|
1260
1545
|
routes.add(normalizeRouteHint2(page.route));
|
|
1261
1546
|
}
|
|
1262
1547
|
}
|
|
1263
1548
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
routes.add(normalizeRouteHint2(page.route));
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
if (v3.blueprint.routes && typeof v3.blueprint.routes === "object") {
|
|
1270
|
-
for (const route of Object.keys(v3.blueprint.routes)) {
|
|
1549
|
+
if (essence.blueprint.routes && typeof essence.blueprint.routes === "object") {
|
|
1550
|
+
for (const route of Object.keys(essence.blueprint.routes)) {
|
|
1271
1551
|
routes.add(normalizeRouteHint2(route));
|
|
1272
1552
|
}
|
|
1273
1553
|
}
|
|
1274
|
-
} else if ("structure" in essence && Array.isArray(essence.structure)) {
|
|
1275
|
-
for (const page of essence.structure) {
|
|
1276
|
-
if (page && typeof page === "object" && "route" in page && typeof page.route === "string" && page.route.length > 0) {
|
|
1277
|
-
routes.add(normalizeRouteHint2(page.route));
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
1554
|
}
|
|
1281
1555
|
return [...routes].filter(Boolean).slice(0, 8);
|
|
1282
1556
|
}
|
|
@@ -1288,14 +1562,13 @@ function summarizeTopology(essence, reviewPack) {
|
|
|
1288
1562
|
let gatewayRouteCount = 0;
|
|
1289
1563
|
let primaryRouteCount = 0;
|
|
1290
1564
|
let hasAnonymousEntryRoute = false;
|
|
1291
|
-
if (essence &&
|
|
1292
|
-
const
|
|
1293
|
-
for (const feature of v3.blueprint.features ?? []) {
|
|
1565
|
+
if (essence && isV4(essence)) {
|
|
1566
|
+
for (const feature of essence.blueprint.features ?? []) {
|
|
1294
1567
|
if (typeof feature === "string" && feature.length > 0) {
|
|
1295
1568
|
features.add(feature);
|
|
1296
1569
|
}
|
|
1297
1570
|
}
|
|
1298
|
-
for (const section of
|
|
1571
|
+
for (const section of essence.blueprint.sections ?? []) {
|
|
1299
1572
|
const role = typeof section.role === "string" && section.role.length > 0 ? section.role : "unknown";
|
|
1300
1573
|
sectionRoles.add(role);
|
|
1301
1574
|
for (const page of section.pages ?? []) {
|
|
@@ -1472,6 +1745,7 @@ function extractFailedRouteDocumentHardening(runtimeAudit) {
|
|
|
1472
1745
|
function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topology, sourceAudit) {
|
|
1473
1746
|
const distPath = join2(projectRoot, "dist");
|
|
1474
1747
|
const indexPath = join2(distPath, "index.html");
|
|
1748
|
+
const isFrameworkBuildOutput = runtimeAudit.failures.includes("next-build-output");
|
|
1475
1749
|
if (!runtimeAudit.distPresent) {
|
|
1476
1750
|
findings.push(
|
|
1477
1751
|
makeFinding({
|
|
@@ -1966,7 +2240,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
|
|
|
1966
2240
|
}
|
|
1967
2241
|
}
|
|
1968
2242
|
const largestIsJs = typeof runtimeAudit.largestAssetPath === "string" && runtimeAudit.largestAssetPath.endsWith(".js");
|
|
1969
|
-
if (largestIsJs && runtimeAudit.largestAssetBytes > PERFORMANCE_BUDGETS.largestJsAssetWarnBytes || runtimeAudit.jsAssetBytes > PERFORMANCE_BUDGETS.totalJsWarnBytes) {
|
|
2243
|
+
if (!isFrameworkBuildOutput && (largestIsJs && runtimeAudit.largestAssetBytes > PERFORMANCE_BUDGETS.largestJsAssetWarnBytes || runtimeAudit.jsAssetBytes > PERFORMANCE_BUDGETS.totalJsWarnBytes)) {
|
|
1970
2244
|
findings.push(
|
|
1971
2245
|
makeFinding({
|
|
1972
2246
|
id: "runtime-js-bundle-large",
|
|
@@ -1981,7 +2255,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
|
|
|
1981
2255
|
})
|
|
1982
2256
|
);
|
|
1983
2257
|
}
|
|
1984
|
-
if (runtimeAudit.cssAssetBytes > PERFORMANCE_BUDGETS.totalCssWarnBytes) {
|
|
2258
|
+
if (!isFrameworkBuildOutput && runtimeAudit.cssAssetBytes > PERFORMANCE_BUDGETS.totalCssWarnBytes) {
|
|
1985
2259
|
findings.push(
|
|
1986
2260
|
makeFinding({
|
|
1987
2261
|
id: "runtime-css-bundle-large",
|
|
@@ -1996,7 +2270,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
|
|
|
1996
2270
|
})
|
|
1997
2271
|
);
|
|
1998
2272
|
}
|
|
1999
|
-
if (runtimeAudit.totalAssetBytes > PERFORMANCE_BUDGETS.totalAssetsWarnBytes) {
|
|
2273
|
+
if (!isFrameworkBuildOutput && runtimeAudit.totalAssetBytes > PERFORMANCE_BUDGETS.totalAssetsWarnBytes) {
|
|
2000
2274
|
findings.push(
|
|
2001
2275
|
makeFinding({
|
|
2002
2276
|
id: "runtime-total-assets-large",
|
|
@@ -2113,7 +2387,7 @@ function appendSourceAuditFindings(findings, sourceAudit, essence, reviewPack) {
|
|
|
2113
2387
|
})
|
|
2114
2388
|
);
|
|
2115
2389
|
}
|
|
2116
|
-
const navigation = essence &&
|
|
2390
|
+
const navigation = essence && isV4(essence) ? essence.meta.navigation : null;
|
|
2117
2391
|
if (navigation?.command_palette && sourceAudit.commandPaletteSignals.count === 0) {
|
|
2118
2392
|
findings.push(
|
|
2119
2393
|
makeFinding({
|
|
@@ -2341,7 +2615,11 @@ function appendSourceAuditFindings(findings, sourceAudit, essence, reviewPack) {
|
|
|
2341
2615
|
})
|
|
2342
2616
|
);
|
|
2343
2617
|
}
|
|
2344
|
-
if (topology.hasAuthFeature && topology.primaryRoutes.length > 0 && sourceAudit.protectedSurfaceSignals.count > 0 && sourceAudit.authGuardSignals.count > 0 && !sourceAuditBucketsOverlap(sourceAudit.protectedSurfaceSignals, sourceAudit.authGuardSignals) && !sourceAuditBucketsOverlap(sourceAudit.protectedSurfaceSignals, sourceAudit.authSessionSignals)
|
|
2618
|
+
if (topology.hasAuthFeature && topology.primaryRoutes.length > 0 && sourceAudit.protectedSurfaceSignals.count > 0 && sourceAudit.authGuardSignals.count > 0 && !sourceAuditBucketsOverlap(sourceAudit.protectedSurfaceSignals, sourceAudit.authGuardSignals) && !sourceAuditBucketsOverlap(sourceAudit.protectedSurfaceSignals, sourceAudit.authSessionSignals) && !sourceAuditProtectedSurfacesCoveredByGuardedNextLayouts(
|
|
2619
|
+
sourceAudit.protectedSurfaceSignals,
|
|
2620
|
+
sourceAudit.authGuardSignals,
|
|
2621
|
+
topology
|
|
2622
|
+
)) {
|
|
2345
2623
|
findings.push(
|
|
2346
2624
|
makeFinding({
|
|
2347
2625
|
id: "source-protected-surface-auth-checks-missing",
|
|
@@ -3209,10 +3487,11 @@ async function auditProject(projectRoot) {
|
|
|
3209
3487
|
})
|
|
3210
3488
|
);
|
|
3211
3489
|
}
|
|
3212
|
-
}
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3490
|
+
} else {
|
|
3491
|
+
const { themeRegistry, patternRegistry } = buildRegistryContext(projectRoot);
|
|
3492
|
+
for (const violation of evaluateGuard(essence, { themeRegistry, patternRegistry })) {
|
|
3493
|
+
findings.push(guardViolationToFinding(violation));
|
|
3494
|
+
}
|
|
3216
3495
|
}
|
|
3217
3496
|
}
|
|
3218
3497
|
appendTopologyFindings(findings, essence, reviewPack);
|
|
@@ -3329,7 +3608,9 @@ function buildDecoratorInventory(treatmentsCss) {
|
|
|
3329
3608
|
decoratorNames.add(name);
|
|
3330
3609
|
}
|
|
3331
3610
|
}
|
|
3332
|
-
return [...decoratorNames].filter(
|
|
3611
|
+
return [...decoratorNames].filter(
|
|
3612
|
+
(name) => !name.startsWith("d-") && !PERSONALITY_UTILS.includes(name)
|
|
3613
|
+
);
|
|
3333
3614
|
}
|
|
3334
3615
|
function isCssClassNameChar(char) {
|
|
3335
3616
|
const codePoint = char.charCodeAt(0);
|
|
@@ -4485,7 +4766,7 @@ function isHeaderClearValue(node) {
|
|
|
4485
4766
|
}
|
|
4486
4767
|
function countAuthGuardSignals(code) {
|
|
4487
4768
|
const patterns = [
|
|
4488
|
-
/\b(?:ProtectedRoute|AuthGuard|RequireAuth|withAuth|requireAuth|ensureAuth|useRequireAuth)\b/,
|
|
4769
|
+
/\b(?:ProtectedRoute|AuthGuard|RequireAuth|withAuth|requireAuth|requireAdmin|ensureAuth|ensureAdmin|useRequireAuth)\b/,
|
|
4489
4770
|
/\b(?:useAuth|useSession|getServerSession|authGuard|isAuthenticated|isSignedIn)\b/,
|
|
4490
4771
|
/\b(?:redirect|navigate|push|replace)\s*\(\s*['"`]\/(?:auth|login|log-?in|sign-?in|sign-?up|register|forgot-password|reset-password)[^'"`]*['"`]/i,
|
|
4491
4772
|
/\bNextResponse\.redirect\s*\(/,
|