@decantr/verifier 1.1.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/dist/index.d.ts +97 -3
- package/dist/index.js +607 -62
- package/dist/index.js.map +1 -1
- package/package.json +16 -3
- package/schema/evidence-bundle.v1.json +197 -0
- package/schema/project-audit-report.v1.json +1 -4
- package/schema/project-health-report.v1.json +11 -1
- package/schema/workspace-health-report.v1.json +140 -0
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, statSync as statSync2 } from "fs";
|
|
3
4
|
import { readFile } from "fs/promises";
|
|
4
|
-
import { extname as extname2, isAbsolute, join as join2, relative, resolve } from "path";
|
|
5
|
-
import { evaluateGuard,
|
|
5
|
+
import { basename, dirname, extname as extname2, isAbsolute, join as join2, relative, resolve } from "path";
|
|
6
|
+
import { evaluateGuard, isV4, validateEssence } from "@decantr/essence-spec";
|
|
6
7
|
import * as ts from "typescript";
|
|
7
8
|
|
|
8
9
|
// src/runtime.ts
|
|
9
|
-
import { existsSync, readFileSync } from "fs";
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
10
11
|
import { createServer } from "http";
|
|
11
12
|
import { extname, join, normalize } from "path";
|
|
12
13
|
var CONTENT_TYPES = {
|
|
@@ -224,6 +225,122 @@ function countSecretLeakSignals(js) {
|
|
|
224
225
|
];
|
|
225
226
|
return patterns.reduce((count, pattern) => count + (js.match(pattern)?.length ?? 0), 0);
|
|
226
227
|
}
|
|
228
|
+
function collectFiles(rootDir, predicate) {
|
|
229
|
+
if (!existsSync(rootDir)) return [];
|
|
230
|
+
const files = [];
|
|
231
|
+
const walk = (dir) => {
|
|
232
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
233
|
+
const path = join(dir, entry.name);
|
|
234
|
+
if (entry.isDirectory()) {
|
|
235
|
+
walk(path);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (entry.isFile() && predicate(path)) {
|
|
239
|
+
files.push(path);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
walk(rootDir);
|
|
244
|
+
return files.sort();
|
|
245
|
+
}
|
|
246
|
+
function auditNextBuildOutput(projectRoot) {
|
|
247
|
+
const nextDir = join(projectRoot, ".next");
|
|
248
|
+
const buildIdPresent = existsSync(join(nextDir, "BUILD_ID"));
|
|
249
|
+
const buildManifestPresent = existsSync(join(nextDir, "build-manifest.json"));
|
|
250
|
+
const appServerDir = join(nextDir, "server", "app");
|
|
251
|
+
const htmlFiles = collectFiles(appServerDir, (filePath) => {
|
|
252
|
+
const relativePath = filePath.slice(appServerDir.length + 1).replace(/\\/g, "/");
|
|
253
|
+
return filePath.endsWith(".html") && !relativePath.startsWith("_");
|
|
254
|
+
});
|
|
255
|
+
const htmlDocuments = htmlFiles.map((filePath) => readFileSync(filePath, "utf-8"));
|
|
256
|
+
const staticAssetFiles = collectFiles(
|
|
257
|
+
join(nextDir, "static"),
|
|
258
|
+
(filePath) => /\.(?:js|css|json|svg|png|jpe?g|webp)$/i.test(filePath)
|
|
259
|
+
);
|
|
260
|
+
let totalAssetBytes = 0;
|
|
261
|
+
let jsAssetBytes = 0;
|
|
262
|
+
let cssAssetBytes = 0;
|
|
263
|
+
let largestAssetPath = null;
|
|
264
|
+
let largestAssetBytes = 0;
|
|
265
|
+
let combinedJs = "";
|
|
266
|
+
for (const assetFile of staticAssetFiles) {
|
|
267
|
+
const byteLength = statSync(assetFile).size;
|
|
268
|
+
const assetPath = `/_next/static/${assetFile.slice(join(nextDir, "static").length + 1).replace(/\\/g, "/")}`;
|
|
269
|
+
totalAssetBytes += byteLength;
|
|
270
|
+
if (assetFile.endsWith(".js")) {
|
|
271
|
+
jsAssetBytes += byteLength;
|
|
272
|
+
combinedJs += readFileSync(assetFile, "utf-8");
|
|
273
|
+
}
|
|
274
|
+
if (assetFile.endsWith(".css")) {
|
|
275
|
+
cssAssetBytes += byteLength;
|
|
276
|
+
}
|
|
277
|
+
if (byteLength > largestAssetBytes) {
|
|
278
|
+
largestAssetBytes = byteLength;
|
|
279
|
+
largestAssetPath = assetPath;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const hasHtmlDocuments = htmlDocuments.length > 0;
|
|
283
|
+
const titleOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<title>[^<]+<\/title>/i.test(html));
|
|
284
|
+
const langOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<html[^>]*\slang=(["'])[^"']+\1/i.test(html));
|
|
285
|
+
const viewportOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<meta[^>]+name=(["'])viewport\1[^>]*>/i.test(html));
|
|
286
|
+
const charsetOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<meta[^>]+charset=/i.test(html));
|
|
287
|
+
const failures = ["next-build-output"];
|
|
288
|
+
if (!buildIdPresent) {
|
|
289
|
+
failures.push("next-build-id-missing");
|
|
290
|
+
}
|
|
291
|
+
if (!buildManifestPresent) {
|
|
292
|
+
failures.push("next-build-manifest-missing");
|
|
293
|
+
}
|
|
294
|
+
if (staticAssetFiles.length === 0) {
|
|
295
|
+
failures.push("next-assets-missing");
|
|
296
|
+
}
|
|
297
|
+
const passed = buildIdPresent && buildManifestPresent && staticAssetFiles.length > 0;
|
|
298
|
+
return {
|
|
299
|
+
distPresent: true,
|
|
300
|
+
indexPresent: true,
|
|
301
|
+
checked: true,
|
|
302
|
+
passed,
|
|
303
|
+
rootDocumentOk: hasHtmlDocuments || passed,
|
|
304
|
+
titleOk,
|
|
305
|
+
langOk,
|
|
306
|
+
viewportOk,
|
|
307
|
+
charsetOk,
|
|
308
|
+
cspSignalOk: true,
|
|
309
|
+
inlineScriptCount: 0,
|
|
310
|
+
inlineEventHandlerCount: 0,
|
|
311
|
+
externalScriptsWithoutIntegrityCount: 0,
|
|
312
|
+
externalScriptsWithIntegrityMissingCrossoriginCount: 0,
|
|
313
|
+
externalStylesheetsWithoutIntegrityCount: 0,
|
|
314
|
+
externalStylesheetsWithIntegrityMissingCrossoriginCount: 0,
|
|
315
|
+
externalScriptsWithInsecureTransportCount: 0,
|
|
316
|
+
externalStylesheetsWithInsecureTransportCount: 0,
|
|
317
|
+
externalMediaSourcesWithInsecureTransportCount: 0,
|
|
318
|
+
externalBlankLinksWithoutRelCount: 0,
|
|
319
|
+
externalIframesWithoutSandboxCount: 0,
|
|
320
|
+
externalIframesWithInsecureTransportCount: 0,
|
|
321
|
+
jsEvalSignalCount: countDynamicCodeSignals(combinedJs),
|
|
322
|
+
jsHtmlInjectionSignalCount: countHtmlInjectionSignals(combinedJs),
|
|
323
|
+
jsInsecureTransportSignalCount: countInsecureTransportSignals(combinedJs),
|
|
324
|
+
jsSecretSignalCount: countSecretLeakSignals(combinedJs),
|
|
325
|
+
assetCount: staticAssetFiles.length,
|
|
326
|
+
assetsPassed: staticAssetFiles.length,
|
|
327
|
+
routeHintsChecked: [],
|
|
328
|
+
routeHintsMatched: 0,
|
|
329
|
+
routeHintsCoverageOk: true,
|
|
330
|
+
routeDocumentsChecked: 0,
|
|
331
|
+
routeDocumentsPassed: 0,
|
|
332
|
+
routeDocumentsHardenedCount: 0,
|
|
333
|
+
routeDocumentsCoverageOk: true,
|
|
334
|
+
routeDocumentsHardeningOk: true,
|
|
335
|
+
fullRouteCoverageOk: true,
|
|
336
|
+
totalAssetBytes,
|
|
337
|
+
jsAssetBytes,
|
|
338
|
+
cssAssetBytes,
|
|
339
|
+
largestAssetPath,
|
|
340
|
+
largestAssetBytes,
|
|
341
|
+
failures
|
|
342
|
+
};
|
|
343
|
+
}
|
|
227
344
|
function normalizeRouteHint(route) {
|
|
228
345
|
if (!route || route === "/") return "/";
|
|
229
346
|
const dynamicIndex = route.indexOf("/:");
|
|
@@ -280,6 +397,9 @@ async function startStaticServer(rootDir) {
|
|
|
280
397
|
async function auditBuiltDist(projectRoot, options = {}) {
|
|
281
398
|
const distDir = options.distDir ?? join(projectRoot, "dist");
|
|
282
399
|
if (!existsSync(distDir)) {
|
|
400
|
+
if (!options.distDir && existsSync(join(projectRoot, ".next"))) {
|
|
401
|
+
return auditNextBuildOutput(projectRoot);
|
|
402
|
+
}
|
|
283
403
|
return emptyRuntimeAudit(["dist-missing"]);
|
|
284
404
|
}
|
|
285
405
|
const indexPath = join(distDir, "index.html");
|
|
@@ -611,9 +731,249 @@ var VERIFICATION_SCHEMA_URLS = {
|
|
|
611
731
|
common: "https://decantr.ai/schemas/verification-report.common.v1.json",
|
|
612
732
|
projectAudit: "https://decantr.ai/schemas/project-audit-report.v1.json",
|
|
613
733
|
projectHealth: "https://decantr.ai/schemas/project-health-report.v1.json",
|
|
734
|
+
evidenceBundle: "https://decantr.ai/schemas/evidence-bundle.v1.json",
|
|
735
|
+
workspaceHealth: "https://decantr.ai/schemas/workspace-health-report.v1.json",
|
|
614
736
|
fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json",
|
|
615
737
|
showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json"
|
|
616
738
|
};
|
|
739
|
+
var EVIDENCE_BUNDLE_SCHEMA_URL = "https://decantr.ai/schemas/evidence-bundle.v1.json";
|
|
740
|
+
function hashString(value) {
|
|
741
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
742
|
+
}
|
|
743
|
+
function hashFile(path) {
|
|
744
|
+
if (!existsSync2(path)) return null;
|
|
745
|
+
try {
|
|
746
|
+
return createHash("sha256").update(readFileSync2(path)).digest("hex");
|
|
747
|
+
} catch {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function provenanceEntry(projectRoot, relativePath) {
|
|
752
|
+
const path = join2(projectRoot, relativePath);
|
|
753
|
+
if (!existsSync2(path)) {
|
|
754
|
+
return {
|
|
755
|
+
path: relativePath,
|
|
756
|
+
present: false,
|
|
757
|
+
hash: null,
|
|
758
|
+
generatedAt: null
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
let generatedAt = null;
|
|
762
|
+
try {
|
|
763
|
+
generatedAt = statSync2(path).mtime.toISOString();
|
|
764
|
+
} catch {
|
|
765
|
+
generatedAt = null;
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
path: relativePath,
|
|
769
|
+
present: true,
|
|
770
|
+
hash: hashFile(path),
|
|
771
|
+
generatedAt
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
function provenanceForPath(projectRoot, path) {
|
|
775
|
+
const rel = isAbsolute(path) ? relative(projectRoot, path).replace(/\\/g, "/") : path;
|
|
776
|
+
return provenanceEntry(projectRoot, rel && !rel.startsWith("..") ? rel : basename(path));
|
|
777
|
+
}
|
|
778
|
+
function redactEvidenceText(projectRoot, value) {
|
|
779
|
+
const normalizedRoot = projectRoot.replace(/\\/g, "/");
|
|
780
|
+
const normalized = value.replace(/\\/g, "/");
|
|
781
|
+
if (normalized.includes(normalizedRoot)) {
|
|
782
|
+
return normalized.split(normalizedRoot).join("<project>");
|
|
783
|
+
}
|
|
784
|
+
return normalized.replace(/\/Users\/[^/\s]+/g, "~").replace(/\/home\/[^/\s]+/g, "~");
|
|
785
|
+
}
|
|
786
|
+
function redactEvidenceList(projectRoot, evidence) {
|
|
787
|
+
return evidence.map((entry) => redactEvidenceText(projectRoot, entry));
|
|
788
|
+
}
|
|
789
|
+
function assertion(input) {
|
|
790
|
+
return {
|
|
791
|
+
status: "passed",
|
|
792
|
+
...input
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function createContractAssertions(projectRoot, audit) {
|
|
796
|
+
const assertions = [];
|
|
797
|
+
const essence = audit?.essence;
|
|
798
|
+
const v4 = essence && isV4(essence) ? essence : null;
|
|
799
|
+
const contextDir = join2(projectRoot, ".decantr", "context");
|
|
800
|
+
const packManifest = audit?.packManifest ?? null;
|
|
801
|
+
assertions.push(
|
|
802
|
+
assertion({
|
|
803
|
+
id: "contract.essence.present",
|
|
804
|
+
category: "context",
|
|
805
|
+
severity: "error",
|
|
806
|
+
rule: "essence-present",
|
|
807
|
+
status: essence ? "passed" : "failed",
|
|
808
|
+
message: essence ? "Decantr Essence contract is present." : "No Decantr Essence contract was found.",
|
|
809
|
+
evidence: [redactEvidenceText(projectRoot, join2(projectRoot, "decantr.essence.json"))],
|
|
810
|
+
suggestedFix: essence ? void 0 : "Run `decantr init` or restore decantr.essence.json."
|
|
811
|
+
})
|
|
812
|
+
);
|
|
813
|
+
assertions.push(
|
|
814
|
+
assertion({
|
|
815
|
+
id: "contract.essence.v4",
|
|
816
|
+
category: "context",
|
|
817
|
+
severity: "error",
|
|
818
|
+
rule: "essence-v4",
|
|
819
|
+
status: v4 ? "passed" : essence ? "failed" : "not_applicable",
|
|
820
|
+
message: v4 ? "Essence uses the active v4.0.0 contract." : essence ? "Essence is not the active v4.0.0 contract." : "Essence version could not be checked.",
|
|
821
|
+
evidence: [essence ? `version=${String(essence.version)}` : "essence missing"],
|
|
822
|
+
suggestedFix: v4 ? void 0 : "Run `decantr migrate --to v4` before reliability checks."
|
|
823
|
+
})
|
|
824
|
+
);
|
|
825
|
+
if (v4) {
|
|
826
|
+
const declaredRoutes = Object.keys(v4.blueprint.routes ?? {}).sort();
|
|
827
|
+
for (const route of declaredRoutes) {
|
|
828
|
+
const routeTarget = (v4.blueprint.routes ?? {})[route];
|
|
829
|
+
const section = v4.blueprint.sections.find((entry) => entry.id === routeTarget.section);
|
|
830
|
+
const page = section?.pages.find((entry) => entry.id === routeTarget.page);
|
|
831
|
+
assertions.push(
|
|
832
|
+
assertion({
|
|
833
|
+
id: `contract.route.${hashString(route)}`,
|
|
834
|
+
category: "route",
|
|
835
|
+
severity: page ? "info" : "warn",
|
|
836
|
+
rule: "declared-route-resolves",
|
|
837
|
+
status: page ? "passed" : "failed",
|
|
838
|
+
target: route,
|
|
839
|
+
message: page ? `Declared route ${route} resolves to a section page.` : `Declared route ${route} does not resolve to an existing section page.`,
|
|
840
|
+
evidence: [`section=${routeTarget.section}`, `page=${routeTarget.page}`],
|
|
841
|
+
suggestedFix: page ? void 0 : "Update blueprint.routes or add the missing section/page before running AI repair."
|
|
842
|
+
})
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
for (const section of v4.blueprint.sections) {
|
|
846
|
+
assertions.push(
|
|
847
|
+
assertion({
|
|
848
|
+
id: `contract.shell.${section.id}`,
|
|
849
|
+
category: "shell",
|
|
850
|
+
severity: section.shell && section.shell !== "inherit" ? "info" : "warn",
|
|
851
|
+
rule: "section-shell-concrete",
|
|
852
|
+
status: section.shell && section.shell !== "inherit" ? "passed" : "failed",
|
|
853
|
+
target: section.id,
|
|
854
|
+
message: section.shell && section.shell !== "inherit" ? `Section ${section.id} uses concrete shell ${section.shell}.` : `Section ${section.id} does not declare a concrete shell.`,
|
|
855
|
+
evidence: [`shell=${section.shell || "missing"}`],
|
|
856
|
+
suggestedFix: section.shell && section.shell !== "inherit" ? void 0 : "Resolve inherited shells during composition so AI agents get concrete layout intent."
|
|
857
|
+
})
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
assertions.push(
|
|
861
|
+
assertion({
|
|
862
|
+
id: "contract.accessibility.focus-visible",
|
|
863
|
+
category: "accessibility",
|
|
864
|
+
severity: v4.dna.accessibility.focus_visible ? "info" : "warn",
|
|
865
|
+
rule: "focus-visible-enabled",
|
|
866
|
+
status: v4.dna.accessibility.focus_visible ? "passed" : "failed",
|
|
867
|
+
message: v4.dna.accessibility.focus_visible ? "DNA requires visible focus states." : "DNA does not require visible focus states.",
|
|
868
|
+
evidence: [`focus_visible=${String(v4.dna.accessibility.focus_visible)}`],
|
|
869
|
+
suggestedFix: v4.dna.accessibility.focus_visible ? void 0 : "Set dna.accessibility.focus_visible=true for stronger UI reliability gates."
|
|
870
|
+
})
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
assertions.push(
|
|
874
|
+
assertion({
|
|
875
|
+
id: "contract.context.pack-manifest",
|
|
876
|
+
category: "context",
|
|
877
|
+
severity: packManifest ? "info" : "warn",
|
|
878
|
+
rule: "pack-manifest-present",
|
|
879
|
+
status: packManifest ? "passed" : "failed",
|
|
880
|
+
message: packManifest ? "Compiled execution pack manifest is present." : "Compiled execution pack manifest is missing.",
|
|
881
|
+
evidence: [redactEvidenceText(projectRoot, join2(contextDir, "pack-manifest.json"))],
|
|
882
|
+
suggestedFix: packManifest ? void 0 : "Run `decantr refresh` to regenerate context packs."
|
|
883
|
+
})
|
|
884
|
+
);
|
|
885
|
+
assertions.push(
|
|
886
|
+
assertion({
|
|
887
|
+
id: "contract.context.review-pack",
|
|
888
|
+
category: "context",
|
|
889
|
+
severity: audit?.reviewPack ? "info" : "warn",
|
|
890
|
+
rule: "review-pack-present",
|
|
891
|
+
status: audit?.reviewPack ? "passed" : "failed",
|
|
892
|
+
message: audit?.reviewPack ? "Compiled review pack is present." : "Compiled review pack is missing.",
|
|
893
|
+
evidence: [redactEvidenceText(projectRoot, join2(contextDir, "review-pack.json"))],
|
|
894
|
+
suggestedFix: audit?.reviewPack ? void 0 : "Run `decantr refresh` or hydrate the review pack from the registry."
|
|
895
|
+
})
|
|
896
|
+
);
|
|
897
|
+
const tokensPath = join2(projectRoot, "src", "styles", "tokens.css");
|
|
898
|
+
assertions.push(
|
|
899
|
+
assertion({
|
|
900
|
+
id: "contract.design-token.tokens-file",
|
|
901
|
+
category: "design-token",
|
|
902
|
+
severity: existsSync2(tokensPath) ? "info" : "warn",
|
|
903
|
+
rule: "tokens-file-present",
|
|
904
|
+
status: existsSync2(tokensPath) ? "passed" : "failed",
|
|
905
|
+
message: existsSync2(tokensPath) ? "Decantr token CSS file is present." : "Decantr token CSS file was not found.",
|
|
906
|
+
evidence: [redactEvidenceText(projectRoot, tokensPath)],
|
|
907
|
+
suggestedFix: existsSync2(tokensPath) ? void 0 : "Run `decantr refresh` or map Decantr tokens into the existing styling system."
|
|
908
|
+
})
|
|
909
|
+
);
|
|
910
|
+
return assertions;
|
|
911
|
+
}
|
|
912
|
+
function createEvidenceBundle(input) {
|
|
913
|
+
const assertions = input.assertions ?? createContractAssertions(input.projectRoot, input.audit);
|
|
914
|
+
let resolvedProjectRoot = input.projectRoot;
|
|
915
|
+
try {
|
|
916
|
+
resolvedProjectRoot = realpathSync(input.projectRoot);
|
|
917
|
+
} catch {
|
|
918
|
+
resolvedProjectRoot = resolve(input.projectRoot);
|
|
919
|
+
}
|
|
920
|
+
const projectId = `project_${hashString(resolvedProjectRoot)}`;
|
|
921
|
+
return {
|
|
922
|
+
$schema: EVIDENCE_BUNDLE_SCHEMA_URL,
|
|
923
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
924
|
+
project: {
|
|
925
|
+
id: projectId,
|
|
926
|
+
rootLabel: basename(input.projectRoot) || "project"
|
|
927
|
+
},
|
|
928
|
+
toolchain: {
|
|
929
|
+
verifierVersion: input.verifierVersion ?? null
|
|
930
|
+
},
|
|
931
|
+
privacy: {
|
|
932
|
+
localOnly: true,
|
|
933
|
+
redactedFields: [
|
|
934
|
+
"source",
|
|
935
|
+
"prompt",
|
|
936
|
+
"secret",
|
|
937
|
+
"environment",
|
|
938
|
+
"absolute_path",
|
|
939
|
+
"repository_name"
|
|
940
|
+
],
|
|
941
|
+
screenshotsLocalOnly: true
|
|
942
|
+
},
|
|
943
|
+
health: {
|
|
944
|
+
status: input.report.status,
|
|
945
|
+
score: input.report.score,
|
|
946
|
+
errorCount: input.report.summary.errorCount,
|
|
947
|
+
warnCount: input.report.summary.warnCount,
|
|
948
|
+
infoCount: input.report.summary.infoCount,
|
|
949
|
+
findingCount: input.report.summary.findingCount
|
|
950
|
+
},
|
|
951
|
+
provenance: {
|
|
952
|
+
essence: provenanceEntry(input.projectRoot, "decantr.essence.json"),
|
|
953
|
+
packManifest: provenanceEntry(input.projectRoot, ".decantr/context/pack-manifest.json"),
|
|
954
|
+
reviewPack: provenanceEntry(input.projectRoot, ".decantr/context/review-pack.json"),
|
|
955
|
+
...input.workspaceConfigPath ? { workspaceConfig: provenanceForPath(input.projectRoot, input.workspaceConfigPath) } : {},
|
|
956
|
+
...input.designTokensPath ? { designTokens: provenanceForPath(input.projectRoot, input.designTokensPath) } : {}
|
|
957
|
+
},
|
|
958
|
+
assertions,
|
|
959
|
+
findings: input.report.findings.map((finding) => ({
|
|
960
|
+
id: finding.id,
|
|
961
|
+
source: finding.source,
|
|
962
|
+
category: finding.category,
|
|
963
|
+
severity: finding.severity,
|
|
964
|
+
message: redactEvidenceText(input.projectRoot, finding.message),
|
|
965
|
+
evidence: redactEvidenceList(input.projectRoot, finding.evidence),
|
|
966
|
+
target: finding.target ? redactEvidenceText(input.projectRoot, finding.target) : void 0,
|
|
967
|
+
rule: finding.rule,
|
|
968
|
+
suggestedFix: finding.suggestedFix,
|
|
969
|
+
remediationSummary: finding.remediation.summary,
|
|
970
|
+
commands: finding.remediation.commands,
|
|
971
|
+
promptCommand: `decantr health --prompt ${finding.id}`
|
|
972
|
+
})),
|
|
973
|
+
...input.browser ? { browser: input.browser } : {},
|
|
974
|
+
...input.designTokens ? { designTokens: input.designTokens } : {}
|
|
975
|
+
};
|
|
976
|
+
}
|
|
617
977
|
var DEFAULT_FOCUS_AREAS = [
|
|
618
978
|
"route-topology",
|
|
619
979
|
"theme-consistency",
|
|
@@ -686,6 +1046,54 @@ function recordSourceAudit(bucket, filePath, count) {
|
|
|
686
1046
|
function sourceAuditBucketsOverlap(a, b) {
|
|
687
1047
|
return a.files.some((file) => b.files.includes(file));
|
|
688
1048
|
}
|
|
1049
|
+
function normalizeSourceAuditPath(filePath) {
|
|
1050
|
+
return filePath.replace(/\\/g, "/");
|
|
1051
|
+
}
|
|
1052
|
+
function getNextAppLayoutGuardRoot(filePath) {
|
|
1053
|
+
const normalized = normalizeSourceAuditPath(filePath);
|
|
1054
|
+
const match = normalized.match(
|
|
1055
|
+
/^(?:src\/)?app\/(?:\([^/]+\)\/)*([^/.()[\]]+)\/layout\.[cm]?[jt]sx?$/i
|
|
1056
|
+
);
|
|
1057
|
+
return match?.[1] ?? null;
|
|
1058
|
+
}
|
|
1059
|
+
function getNextAppRouteRoot(filePath) {
|
|
1060
|
+
const normalized = normalizeSourceAuditPath(filePath);
|
|
1061
|
+
const match = normalized.match(/^(?:src\/)?app\/(?:\([^/]+\)\/)*([^/.()[\]]+)(?:\/|$)/i);
|
|
1062
|
+
return match?.[1] ?? null;
|
|
1063
|
+
}
|
|
1064
|
+
function getRouteRoot(route) {
|
|
1065
|
+
const firstSegment = route.replace(/^\/+/, "").split("/")[0];
|
|
1066
|
+
if (!firstSegment || firstSegment.startsWith(":") || firstSegment.startsWith("[")) {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
return firstSegment;
|
|
1070
|
+
}
|
|
1071
|
+
function isNextAppPublicChromeFile(filePath) {
|
|
1072
|
+
const normalized = normalizeSourceAuditPath(filePath);
|
|
1073
|
+
return /^(?:src\/)?app\/(?:\([^/]+\)\/)*(?:layout|nav|nav-header|navigation|header|footer|sidebar)\.[cm]?[jt]sx?$/i.test(
|
|
1074
|
+
normalized
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
function sourceAuditProtectedSurfacesCoveredByGuardedNextLayouts(protectedSurfaces, authGuards, topology) {
|
|
1078
|
+
const guardedRoots = /* @__PURE__ */ new Set();
|
|
1079
|
+
for (const file of authGuards.files) {
|
|
1080
|
+
const root = getNextAppLayoutGuardRoot(file);
|
|
1081
|
+
if (root) {
|
|
1082
|
+
guardedRoots.add(root);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (guardedRoots.size === 0 || protectedSurfaces.files.length === 0) {
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
const guardedPrimaryRouteRoots = topology.primaryRoutes.map(getRouteRoot).filter((root) => Boolean(root)).filter((root) => guardedRoots.has(root));
|
|
1089
|
+
return protectedSurfaces.files.every((file) => {
|
|
1090
|
+
const appRoot = getNextAppRouteRoot(file);
|
|
1091
|
+
if (appRoot && guardedRoots.has(appRoot)) {
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
return isNextAppPublicChromeFile(file) && guardedPrimaryRouteRoots.length > 0;
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
689
1097
|
function isAuditableSourceFile(filePath) {
|
|
690
1098
|
if (/\.d\.ts$/i.test(filePath)) return false;
|
|
691
1099
|
return /\.(?:[cm]?[jt]sx?)$/i.test(filePath);
|
|
@@ -726,7 +1134,7 @@ function collectProjectSourceFiles(projectRoot) {
|
|
|
726
1134
|
"coverage"
|
|
727
1135
|
]);
|
|
728
1136
|
const walk = (dir) => {
|
|
729
|
-
for (const entry of
|
|
1137
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
730
1138
|
if (ignoredDirNames.has(entry.name)) continue;
|
|
731
1139
|
const absolutePath = join2(dir, entry.name);
|
|
732
1140
|
if (entry.isDirectory()) {
|
|
@@ -770,7 +1178,7 @@ function collectProjectStyleFiles(projectRoot) {
|
|
|
770
1178
|
"coverage"
|
|
771
1179
|
]);
|
|
772
1180
|
const walk = (dir) => {
|
|
773
|
-
for (const entry of
|
|
1181
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
774
1182
|
if (ignoredDirNames.has(entry.name)) continue;
|
|
775
1183
|
const absolutePath = join2(dir, entry.name);
|
|
776
1184
|
if (entry.isDirectory()) {
|
|
@@ -799,8 +1207,126 @@ function countReducedMotionSignals(code) {
|
|
|
799
1207
|
];
|
|
800
1208
|
return patterns.reduce((count, pattern) => count + (pattern.test(code) ? 1 : 0), 0);
|
|
801
1209
|
}
|
|
1210
|
+
function hasModuleDirective(filePath, code, directive) {
|
|
1211
|
+
const sourceFile = ts.createSourceFile(
|
|
1212
|
+
filePath,
|
|
1213
|
+
code,
|
|
1214
|
+
ts.ScriptTarget.Latest,
|
|
1215
|
+
true,
|
|
1216
|
+
getScriptKind(filePath)
|
|
1217
|
+
);
|
|
1218
|
+
for (const statement of sourceFile.statements) {
|
|
1219
|
+
if (ts.isExpressionStatement(statement) && ts.isStringLiteralLike(statement.expression)) {
|
|
1220
|
+
if (statement.expression.text === directive) {
|
|
1221
|
+
return true;
|
|
1222
|
+
}
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
break;
|
|
1226
|
+
}
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
function importDeclarationHasRuntimeBinding(statement) {
|
|
1230
|
+
const clause = statement.importClause;
|
|
1231
|
+
if (!clause) return true;
|
|
1232
|
+
if (clause.isTypeOnly) return false;
|
|
1233
|
+
if (clause.name) return true;
|
|
1234
|
+
if (!clause.namedBindings) return true;
|
|
1235
|
+
if (ts.isNamespaceImport(clause.namedBindings)) return true;
|
|
1236
|
+
return clause.namedBindings.elements.some((element) => !element.isTypeOnly);
|
|
1237
|
+
}
|
|
1238
|
+
function collectRuntimeImportSpecifiers(entry) {
|
|
1239
|
+
const sourceFile = ts.createSourceFile(
|
|
1240
|
+
entry.relativePath,
|
|
1241
|
+
entry.code,
|
|
1242
|
+
ts.ScriptTarget.Latest,
|
|
1243
|
+
true,
|
|
1244
|
+
getScriptKind(entry.relativePath)
|
|
1245
|
+
);
|
|
1246
|
+
const specifiers = [];
|
|
1247
|
+
for (const statement of sourceFile.statements) {
|
|
1248
|
+
if (ts.isImportDeclaration(statement) && ts.isStringLiteralLike(statement.moduleSpecifier) && importDeclarationHasRuntimeBinding(statement)) {
|
|
1249
|
+
specifiers.push(statement.moduleSpecifier.text);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return specifiers;
|
|
1253
|
+
}
|
|
1254
|
+
function resolveSourceImportTarget(projectRoot, sourceAbsolutePath, specifier) {
|
|
1255
|
+
let basePath = null;
|
|
1256
|
+
if (specifier.startsWith("@/")) {
|
|
1257
|
+
basePath = join2(projectRoot, "src", specifier.slice(2));
|
|
1258
|
+
} else if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
|
1259
|
+
basePath = resolve(dirname(sourceAbsolutePath), specifier);
|
|
1260
|
+
}
|
|
1261
|
+
if (!basePath) return null;
|
|
1262
|
+
const candidates = [
|
|
1263
|
+
basePath,
|
|
1264
|
+
`${basePath}.ts`,
|
|
1265
|
+
`${basePath}.tsx`,
|
|
1266
|
+
`${basePath}.js`,
|
|
1267
|
+
`${basePath}.jsx`,
|
|
1268
|
+
`${basePath}.mts`,
|
|
1269
|
+
`${basePath}.cts`,
|
|
1270
|
+
join2(basePath, "index.ts"),
|
|
1271
|
+
join2(basePath, "index.tsx"),
|
|
1272
|
+
join2(basePath, "index.js"),
|
|
1273
|
+
join2(basePath, "index.jsx")
|
|
1274
|
+
];
|
|
1275
|
+
for (const candidate of candidates) {
|
|
1276
|
+
if (existsSync2(candidate) && isAuditableSourceFile(candidate)) {
|
|
1277
|
+
return relative(projectRoot, candidate) || candidate;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
function collectClientReachableSourceFiles(projectRoot, entries) {
|
|
1283
|
+
const entriesByRelativePath = new Map(entries.map((entry) => [entry.relativePath, entry]));
|
|
1284
|
+
const runtimeImportGraph = /* @__PURE__ */ new Map();
|
|
1285
|
+
for (const entry of entries) {
|
|
1286
|
+
const targets = collectRuntimeImportSpecifiers(entry).map((specifier) => resolveSourceImportTarget(projectRoot, entry.absolutePath, specifier)).filter((target) => Boolean(target));
|
|
1287
|
+
runtimeImportGraph.set(entry.relativePath, targets);
|
|
1288
|
+
}
|
|
1289
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
1290
|
+
const queue = entries.filter((entry) => hasModuleDirective(entry.relativePath, entry.code, "use client")).map((entry) => entry.relativePath);
|
|
1291
|
+
for (const root of queue) {
|
|
1292
|
+
reachable.add(root);
|
|
1293
|
+
}
|
|
1294
|
+
while (queue.length > 0) {
|
|
1295
|
+
const current = queue.shift();
|
|
1296
|
+
for (const target of runtimeImportGraph.get(current) ?? []) {
|
|
1297
|
+
const targetEntry = entriesByRelativePath.get(target);
|
|
1298
|
+
if (targetEntry && hasModuleDirective(target, targetEntry.code, "use server")) {
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
if (!reachable.has(target)) {
|
|
1302
|
+
reachable.add(target);
|
|
1303
|
+
queue.push(target);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return reachable;
|
|
1308
|
+
}
|
|
1309
|
+
function isClientAuthHeaderSource(relativePath, code, clientReachableFiles) {
|
|
1310
|
+
if (hasModuleDirective(relativePath, code, "use server")) {
|
|
1311
|
+
return false;
|
|
1312
|
+
}
|
|
1313
|
+
if (clientReachableFiles.has(relativePath)) {
|
|
1314
|
+
return true;
|
|
1315
|
+
}
|
|
1316
|
+
const normalized = normalizeSourceAuditPath(relativePath);
|
|
1317
|
+
if (/^(?:src\/)?app\//i.test(normalized)) {
|
|
1318
|
+
return false;
|
|
1319
|
+
}
|
|
1320
|
+
return /(?:^|\/)(?:components|routes|pages|hooks|providers)\//i.test(normalized);
|
|
1321
|
+
}
|
|
802
1322
|
function auditProjectSourceTree(projectRoot) {
|
|
803
1323
|
const sourceFiles = collectProjectSourceFiles(projectRoot);
|
|
1324
|
+
const sourceEntries = sourceFiles.map((sourceFile) => ({
|
|
1325
|
+
absolutePath: sourceFile,
|
|
1326
|
+
relativePath: relative(projectRoot, sourceFile) || sourceFile,
|
|
1327
|
+
code: readFileSync2(sourceFile, "utf-8")
|
|
1328
|
+
}));
|
|
1329
|
+
const clientReachableFiles = collectClientReachableSourceFiles(projectRoot, sourceEntries);
|
|
804
1330
|
const summary = {
|
|
805
1331
|
filesChecked: sourceFiles.length,
|
|
806
1332
|
inlineStyles: createSourceAuditBucket(),
|
|
@@ -869,10 +1395,13 @@ function auditProjectSourceTree(projectRoot) {
|
|
|
869
1395
|
interactionSafetyIssues: createSourceAuditBucket(),
|
|
870
1396
|
authInputHintIssues: createSourceAuditBucket()
|
|
871
1397
|
};
|
|
872
|
-
for (const
|
|
873
|
-
const relativePath = relative(projectRoot, sourceFile) || sourceFile;
|
|
874
|
-
const code = readFileSync2(sourceFile, "utf-8");
|
|
1398
|
+
for (const { relativePath, code } of sourceEntries) {
|
|
875
1399
|
const signals = analyzeAstSignals(relativePath, code);
|
|
1400
|
+
const isClientAuthHeaderSourceFile = isClientAuthHeaderSource(
|
|
1401
|
+
relativePath,
|
|
1402
|
+
code,
|
|
1403
|
+
clientReachableFiles
|
|
1404
|
+
);
|
|
876
1405
|
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
1406
|
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
1407
|
const authInputHintIssueCount = signals.emailAutocompleteMissingCount + signals.passwordAutocompleteMissingCount + signals.otpAutocompleteMissingCount + signals.authAutocompleteDisabledCount + signals.authAutocompleteSemanticMismatchCount + signals.authInputTypeMismatchCount;
|
|
@@ -1042,8 +1571,16 @@ function auditProjectSourceTree(projectRoot) {
|
|
|
1042
1571
|
recordSourceAudit(summary.authStorageClears, relativePath, signals.authStorageClearCount);
|
|
1043
1572
|
recordSourceAudit(summary.authCookieWrites, relativePath, signals.authCookieWriteCount);
|
|
1044
1573
|
recordSourceAudit(summary.authCookieClears, relativePath, signals.authCookieClearCount);
|
|
1045
|
-
recordSourceAudit(
|
|
1046
|
-
|
|
1574
|
+
recordSourceAudit(
|
|
1575
|
+
summary.authHeaderWrites,
|
|
1576
|
+
relativePath,
|
|
1577
|
+
isClientAuthHeaderSourceFile ? signals.authHeaderWriteCount : 0
|
|
1578
|
+
);
|
|
1579
|
+
recordSourceAudit(
|
|
1580
|
+
summary.authHeaderClears,
|
|
1581
|
+
relativePath,
|
|
1582
|
+
isClientAuthHeaderSourceFile ? signals.authHeaderClearCount : 0
|
|
1583
|
+
);
|
|
1047
1584
|
recordSourceAudit(summary.authCacheClients, relativePath, signals.authCacheClientCount);
|
|
1048
1585
|
recordSourceAudit(summary.authCacheClears, relativePath, signals.authCacheClearCount);
|
|
1049
1586
|
recordSourceAudit(summary.authRefreshSignals, relativePath, signals.authRefreshSignalCount);
|
|
@@ -1115,7 +1652,7 @@ function buildRegistryContext(projectRoot) {
|
|
|
1115
1652
|
const cachedThemesDir = join2(cacheDir, "@official", "themes");
|
|
1116
1653
|
try {
|
|
1117
1654
|
if (existsSync2(cachedThemesDir)) {
|
|
1118
|
-
for (const file of
|
|
1655
|
+
for (const file of readdirSync2(cachedThemesDir).filter(
|
|
1119
1656
|
(name) => name.endsWith(".json") && name !== "index.json"
|
|
1120
1657
|
)) {
|
|
1121
1658
|
const data = JSON.parse(readFileSync2(join2(cachedThemesDir, file), "utf-8"));
|
|
@@ -1129,7 +1666,7 @@ function buildRegistryContext(projectRoot) {
|
|
|
1129
1666
|
const customThemesDir = join2(customDir, "themes");
|
|
1130
1667
|
try {
|
|
1131
1668
|
if (existsSync2(customThemesDir)) {
|
|
1132
|
-
for (const file of
|
|
1669
|
+
for (const file of readdirSync2(customThemesDir).filter((name) => name.endsWith(".json"))) {
|
|
1133
1670
|
const data = JSON.parse(readFileSync2(join2(customThemesDir, file), "utf-8"));
|
|
1134
1671
|
if (data.id) {
|
|
1135
1672
|
themeRegistry.set(`custom:${data.id}`, { modes: data.modes || ["light", "dark"] });
|
|
@@ -1141,7 +1678,7 @@ function buildRegistryContext(projectRoot) {
|
|
|
1141
1678
|
const cachedPatternsDir = join2(cacheDir, "@official", "patterns");
|
|
1142
1679
|
try {
|
|
1143
1680
|
if (existsSync2(cachedPatternsDir)) {
|
|
1144
|
-
for (const file of
|
|
1681
|
+
for (const file of readdirSync2(cachedPatternsDir).filter(
|
|
1145
1682
|
(name) => name.endsWith(".json") && name !== "index.json"
|
|
1146
1683
|
)) {
|
|
1147
1684
|
const data = JSON.parse(readFileSync2(join2(cachedPatternsDir, file), "utf-8"));
|
|
@@ -1167,17 +1704,13 @@ function guardViolationToFinding(violation) {
|
|
|
1167
1704
|
}
|
|
1168
1705
|
function countPages(essence) {
|
|
1169
1706
|
if (!essence) return 0;
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
if ("structure" in essence && Array.isArray(essence.structure)) {
|
|
1178
|
-
return essence.structure.length;
|
|
1179
|
-
}
|
|
1180
|
-
return 0;
|
|
1707
|
+
const blueprint = essence.blueprint;
|
|
1708
|
+
const sectionPages = Array.isArray(blueprint.sections) ? blueprint.sections.reduce(
|
|
1709
|
+
(sum, section) => sum + (Array.isArray(section.pages) ? section.pages.length : 0),
|
|
1710
|
+
0
|
|
1711
|
+
) : 0;
|
|
1712
|
+
const flatPages = Array.isArray(blueprint.pages) ? blueprint.pages.length : 0;
|
|
1713
|
+
return sectionPages + flatPages;
|
|
1181
1714
|
}
|
|
1182
1715
|
function makeFinding(input) {
|
|
1183
1716
|
return input;
|
|
@@ -1252,31 +1785,19 @@ function extractRouteHintsFromEssence(essence) {
|
|
|
1252
1785
|
return ["/"];
|
|
1253
1786
|
}
|
|
1254
1787
|
const routes = /* @__PURE__ */ new Set(["/"]);
|
|
1255
|
-
if (
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1258
|
-
for (const page of section.pages ?? []) {
|
|
1788
|
+
if (isV4(essence)) {
|
|
1789
|
+
for (const section of essence.blueprint.sections) {
|
|
1790
|
+
for (const page of section.pages) {
|
|
1259
1791
|
if (typeof page.route === "string" && page.route.length > 0) {
|
|
1260
1792
|
routes.add(normalizeRouteHint2(page.route));
|
|
1261
1793
|
}
|
|
1262
1794
|
}
|
|
1263
1795
|
}
|
|
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)) {
|
|
1796
|
+
if (essence.blueprint.routes && typeof essence.blueprint.routes === "object") {
|
|
1797
|
+
for (const route of Object.keys(essence.blueprint.routes)) {
|
|
1271
1798
|
routes.add(normalizeRouteHint2(route));
|
|
1272
1799
|
}
|
|
1273
1800
|
}
|
|
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
1801
|
}
|
|
1281
1802
|
return [...routes].filter(Boolean).slice(0, 8);
|
|
1282
1803
|
}
|
|
@@ -1288,14 +1809,13 @@ function summarizeTopology(essence, reviewPack) {
|
|
|
1288
1809
|
let gatewayRouteCount = 0;
|
|
1289
1810
|
let primaryRouteCount = 0;
|
|
1290
1811
|
let hasAnonymousEntryRoute = false;
|
|
1291
|
-
if (essence &&
|
|
1292
|
-
const
|
|
1293
|
-
for (const feature of v3.blueprint.features ?? []) {
|
|
1812
|
+
if (essence && isV4(essence)) {
|
|
1813
|
+
for (const feature of essence.blueprint.features ?? []) {
|
|
1294
1814
|
if (typeof feature === "string" && feature.length > 0) {
|
|
1295
1815
|
features.add(feature);
|
|
1296
1816
|
}
|
|
1297
1817
|
}
|
|
1298
|
-
for (const section of
|
|
1818
|
+
for (const section of essence.blueprint.sections ?? []) {
|
|
1299
1819
|
const role = typeof section.role === "string" && section.role.length > 0 ? section.role : "unknown";
|
|
1300
1820
|
sectionRoles.add(role);
|
|
1301
1821
|
for (const page of section.pages ?? []) {
|
|
@@ -1472,6 +1992,7 @@ function extractFailedRouteDocumentHardening(runtimeAudit) {
|
|
|
1472
1992
|
function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topology, sourceAudit) {
|
|
1473
1993
|
const distPath = join2(projectRoot, "dist");
|
|
1474
1994
|
const indexPath = join2(distPath, "index.html");
|
|
1995
|
+
const isFrameworkBuildOutput = runtimeAudit.failures.includes("next-build-output");
|
|
1475
1996
|
if (!runtimeAudit.distPresent) {
|
|
1476
1997
|
findings.push(
|
|
1477
1998
|
makeFinding({
|
|
@@ -1966,7 +2487,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
|
|
|
1966
2487
|
}
|
|
1967
2488
|
}
|
|
1968
2489
|
const largestIsJs = typeof runtimeAudit.largestAssetPath === "string" && runtimeAudit.largestAssetPath.endsWith(".js");
|
|
1969
|
-
if (largestIsJs && runtimeAudit.largestAssetBytes > PERFORMANCE_BUDGETS.largestJsAssetWarnBytes || runtimeAudit.jsAssetBytes > PERFORMANCE_BUDGETS.totalJsWarnBytes) {
|
|
2490
|
+
if (!isFrameworkBuildOutput && (largestIsJs && runtimeAudit.largestAssetBytes > PERFORMANCE_BUDGETS.largestJsAssetWarnBytes || runtimeAudit.jsAssetBytes > PERFORMANCE_BUDGETS.totalJsWarnBytes)) {
|
|
1970
2491
|
findings.push(
|
|
1971
2492
|
makeFinding({
|
|
1972
2493
|
id: "runtime-js-bundle-large",
|
|
@@ -1981,7 +2502,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
|
|
|
1981
2502
|
})
|
|
1982
2503
|
);
|
|
1983
2504
|
}
|
|
1984
|
-
if (runtimeAudit.cssAssetBytes > PERFORMANCE_BUDGETS.totalCssWarnBytes) {
|
|
2505
|
+
if (!isFrameworkBuildOutput && runtimeAudit.cssAssetBytes > PERFORMANCE_BUDGETS.totalCssWarnBytes) {
|
|
1985
2506
|
findings.push(
|
|
1986
2507
|
makeFinding({
|
|
1987
2508
|
id: "runtime-css-bundle-large",
|
|
@@ -1996,7 +2517,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
|
|
|
1996
2517
|
})
|
|
1997
2518
|
);
|
|
1998
2519
|
}
|
|
1999
|
-
if (runtimeAudit.totalAssetBytes > PERFORMANCE_BUDGETS.totalAssetsWarnBytes) {
|
|
2520
|
+
if (!isFrameworkBuildOutput && runtimeAudit.totalAssetBytes > PERFORMANCE_BUDGETS.totalAssetsWarnBytes) {
|
|
2000
2521
|
findings.push(
|
|
2001
2522
|
makeFinding({
|
|
2002
2523
|
id: "runtime-total-assets-large",
|
|
@@ -2113,7 +2634,7 @@ function appendSourceAuditFindings(findings, sourceAudit, essence, reviewPack) {
|
|
|
2113
2634
|
})
|
|
2114
2635
|
);
|
|
2115
2636
|
}
|
|
2116
|
-
const navigation = essence &&
|
|
2637
|
+
const navigation = essence && isV4(essence) ? essence.meta.navigation : null;
|
|
2117
2638
|
if (navigation?.command_palette && sourceAudit.commandPaletteSignals.count === 0) {
|
|
2118
2639
|
findings.push(
|
|
2119
2640
|
makeFinding({
|
|
@@ -2341,7 +2862,11 @@ function appendSourceAuditFindings(findings, sourceAudit, essence, reviewPack) {
|
|
|
2341
2862
|
})
|
|
2342
2863
|
);
|
|
2343
2864
|
}
|
|
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)
|
|
2865
|
+
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(
|
|
2866
|
+
sourceAudit.protectedSurfaceSignals,
|
|
2867
|
+
sourceAudit.authGuardSignals,
|
|
2868
|
+
topology
|
|
2869
|
+
)) {
|
|
2345
2870
|
findings.push(
|
|
2346
2871
|
makeFinding({
|
|
2347
2872
|
id: "source-protected-surface-auth-checks-missing",
|
|
@@ -3209,10 +3734,11 @@ async function auditProject(projectRoot) {
|
|
|
3209
3734
|
})
|
|
3210
3735
|
);
|
|
3211
3736
|
}
|
|
3212
|
-
}
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3737
|
+
} else {
|
|
3738
|
+
const { themeRegistry, patternRegistry } = buildRegistryContext(projectRoot);
|
|
3739
|
+
for (const violation of evaluateGuard(essence, { themeRegistry, patternRegistry })) {
|
|
3740
|
+
findings.push(guardViolationToFinding(violation));
|
|
3741
|
+
}
|
|
3216
3742
|
}
|
|
3217
3743
|
}
|
|
3218
3744
|
appendTopologyFindings(findings, essence, reviewPack);
|
|
@@ -3329,7 +3855,9 @@ function buildDecoratorInventory(treatmentsCss) {
|
|
|
3329
3855
|
decoratorNames.add(name);
|
|
3330
3856
|
}
|
|
3331
3857
|
}
|
|
3332
|
-
return [...decoratorNames].filter(
|
|
3858
|
+
return [...decoratorNames].filter(
|
|
3859
|
+
(name) => !name.startsWith("d-") && !PERSONALITY_UTILS.includes(name)
|
|
3860
|
+
);
|
|
3333
3861
|
}
|
|
3334
3862
|
function isCssClassNameChar(char) {
|
|
3335
3863
|
const codePoint = char.charCodeAt(0);
|
|
@@ -3603,6 +4131,23 @@ function isNavigationLandmark(tagName, attributes) {
|
|
|
3603
4131
|
function hasNavigationLabel(attributes) {
|
|
3604
4132
|
return Boolean(getJsxAttribute(attributes, "aria-label", "aria-labelledby", "title"));
|
|
3605
4133
|
}
|
|
4134
|
+
function getJsxAttributeOwner(attribute) {
|
|
4135
|
+
const attributes = attribute.parent;
|
|
4136
|
+
if (!attributes || !ts.isJsxAttributes(attributes)) return null;
|
|
4137
|
+
const owner = attributes.parent;
|
|
4138
|
+
if (ts.isJsxOpeningElement(owner) || ts.isJsxSelfClosingElement(owner)) {
|
|
4139
|
+
return owner;
|
|
4140
|
+
}
|
|
4141
|
+
return null;
|
|
4142
|
+
}
|
|
4143
|
+
function isSafeJsonLdDangerouslySetInnerHtml(attribute) {
|
|
4144
|
+
const owner = getJsxAttributeOwner(attribute);
|
|
4145
|
+
if (!owner || getJsxTagName(owner) !== "script") return false;
|
|
4146
|
+
const typeValue = getJsxAttributeLiteralValue(getJsxAttribute(owner.attributes, "type"))?.trim().toLowerCase();
|
|
4147
|
+
if (typeValue !== "application/ld+json") return false;
|
|
4148
|
+
const sourceText = attribute.getText();
|
|
4149
|
+
return /\bJSON\.stringify\s*\(/.test(sourceText) && /\\u003c/i.test(sourceText);
|
|
4150
|
+
}
|
|
3606
4151
|
function hasInsecureFormAction(attributes) {
|
|
3607
4152
|
const actionValue = getJsxAttributeLiteralValue(getJsxAttribute(attributes, "action"));
|
|
3608
4153
|
const normalized = actionValue?.trim().toLowerCase() ?? "";
|
|
@@ -4485,7 +5030,7 @@ function isHeaderClearValue(node) {
|
|
|
4485
5030
|
}
|
|
4486
5031
|
function countAuthGuardSignals(code) {
|
|
4487
5032
|
const patterns = [
|
|
4488
|
-
/\b(?:ProtectedRoute|AuthGuard|RequireAuth|withAuth|requireAuth|ensureAuth|useRequireAuth)\b/,
|
|
5033
|
+
/\b(?:ProtectedRoute|AuthGuard|RequireAuth|withAuth|requireAuth|requireAdmin|ensureAuth|ensureAdmin|useRequireAuth)\b/,
|
|
4489
5034
|
/\b(?:useAuth|useSession|getServerSession|authGuard|isAuthenticated|isSignedIn)\b/,
|
|
4490
5035
|
/\b(?:redirect|navigate|push|replace)\s*\(\s*['"`]\/(?:auth|login|log-?in|sign-?in|sign-?up|register|forgot-password|reset-password)[^'"`]*['"`]/i,
|
|
4491
5036
|
/\bNextResponse\.redirect\s*\(/,
|
|
@@ -9125,7 +9670,7 @@ function analyzeAstSignals(filePath, code) {
|
|
|
9125
9670
|
signals.inlineStyleAttributeCount += 1;
|
|
9126
9671
|
}
|
|
9127
9672
|
}
|
|
9128
|
-
if (isPropertyNamed(node.name, "dangerouslySetInnerHTML")) {
|
|
9673
|
+
if (isPropertyNamed(node.name, "dangerouslySetInnerHTML") && !isSafeJsonLdDangerouslySetInnerHtml(node)) {
|
|
9129
9674
|
signals.dangerousHtmlCount += 1;
|
|
9130
9675
|
}
|
|
9131
9676
|
if (isPropertyNamed(node.name, "href", "to") && node.initializer && ts.isJsxExpression(node.initializer) && expressionContainsOpenRedirectSource(
|
|
@@ -10755,10 +11300,7 @@ function critiqueSource({
|
|
|
10755
11300
|
})
|
|
10756
11301
|
);
|
|
10757
11302
|
}
|
|
10758
|
-
const dangerousHtmlCount =
|
|
10759
|
-
astSignals.dangerousHtmlCount,
|
|
10760
|
-
/dangerouslySetInnerHTML\s*=/.test(code) ? 1 : 0
|
|
10761
|
-
);
|
|
11303
|
+
const dangerousHtmlCount = astSignals.dangerousHtmlCount;
|
|
10762
11304
|
const rawHtmlInjectionCount = Math.max(
|
|
10763
11305
|
astSignals.rawHtmlInjectionCount,
|
|
10764
11306
|
/\binnerHTML\s*=|\binsertAdjacentHTML\s*\(|\bdocument\.write\s*\(/.test(code) ? 1 : 0
|
|
@@ -11385,10 +11927,13 @@ async function critiqueFile(filePath, projectRoot) {
|
|
|
11385
11927
|
});
|
|
11386
11928
|
}
|
|
11387
11929
|
export {
|
|
11930
|
+
EVIDENCE_BUNDLE_SCHEMA_URL,
|
|
11388
11931
|
INTERACTION_SIGNALS,
|
|
11389
11932
|
VERIFICATION_SCHEMA_URLS,
|
|
11390
11933
|
auditBuiltDist,
|
|
11391
11934
|
auditProject,
|
|
11935
|
+
createContractAssertions,
|
|
11936
|
+
createEvidenceBundle,
|
|
11392
11937
|
critiqueFile,
|
|
11393
11938
|
critiqueSource,
|
|
11394
11939
|
emptyRuntimeAudit,
|