laxy-verify 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +181 -47
  2. package/dist/a11y-deep.d.ts +20 -0
  3. package/dist/a11y-deep.js +161 -0
  4. package/dist/ai-analysis.d.ts +28 -0
  5. package/dist/ai-analysis.js +32 -0
  6. package/dist/audit/broken-links.d.ts +5 -1
  7. package/dist/audit/broken-links.js +23 -12
  8. package/dist/bundle-size.d.ts +14 -0
  9. package/dist/bundle-size.js +209 -0
  10. package/dist/cli.js +391 -13
  11. package/dist/compare-env.d.ts +23 -0
  12. package/dist/compare-env.js +55 -0
  13. package/dist/config.d.ts +37 -0
  14. package/dist/config.js +106 -1
  15. package/dist/entitlement.d.ts +2 -0
  16. package/dist/entitlement.js +5 -1
  17. package/dist/init-analysis.d.ts +6 -0
  18. package/dist/init-analysis.js +302 -0
  19. package/dist/init.js +66 -0
  20. package/dist/lighthouse.d.ts +31 -1
  21. package/dist/lighthouse.js +76 -3
  22. package/dist/outdated-check.d.ts +17 -0
  23. package/dist/outdated-check.js +123 -0
  24. package/dist/report-markdown.d.ts +14 -0
  25. package/dist/report-markdown.js +21 -0
  26. package/dist/route-discovery.d.ts +7 -0
  27. package/dist/route-discovery.js +108 -0
  28. package/dist/secret-scan.d.ts +15 -0
  29. package/dist/secret-scan.js +218 -0
  30. package/dist/security-audit.d.ts +9 -1
  31. package/dist/security-audit.js +87 -24
  32. package/dist/seo-deep.d.ts +24 -0
  33. package/dist/seo-deep.js +147 -0
  34. package/dist/typecheck.d.ts +8 -0
  35. package/dist/typecheck.js +99 -0
  36. package/dist/verification-core/report.js +117 -0
  37. package/dist/verification-core/types.d.ts +58 -2
  38. package/dist/visual-diff.d.ts +8 -1
  39. package/dist/visual-diff.js +53 -8
  40. package/dist/vitals-budget.d.ts +23 -0
  41. package/dist/vitals-budget.js +168 -0
  42. package/package.json +1 -1
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.runLighthouse = runLighthouse;
37
+ exports.runLighthouseOnUrl = runLighthouseOnUrl;
38
+ exports.runLighthouseOnRoutes = runLighthouseOnRoutes;
37
39
  const node_child_process_1 = require("node:child_process");
38
40
  const fs = __importStar(require("node:fs"));
39
41
  const path = __importStar(require("node:path"));
@@ -114,13 +116,13 @@ const [url, reportPath, chromeDir] = process.argv.slice(2);
114
116
  // .cjs 확장자로 저장해서 Node가 CommonJS로 실행하도록 함
115
117
  fs.writeFileSync(runnerPath, source, "utf-8");
116
118
  }
117
- async function runLighthouse(port, runs) {
119
+ async function runLighthouseCore(fullUrl, runs, label) {
118
120
  const baseTmpDir = path.join(process.cwd(), ".laxy-tmp", `lighthouse-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
119
121
  const reportsDir = path.join(baseTmpDir, "reports");
120
122
  const runnerPath = path.join(baseTmpDir, "run-lighthouse.cjs");
121
123
  fs.mkdirSync(reportsDir, { recursive: true });
122
124
  writeRunnerScript(runnerPath);
123
- console.log(`\n Running Lighthouse (${runs} run${runs > 1 ? "s" : ""})...`);
125
+ console.log(`\n Running Lighthouse on ${label} (${runs} run${runs > 1 ? "s" : ""})...`);
124
126
  const errors = [];
125
127
  const allScores = [];
126
128
  for (let runIndex = 0; runIndex < runs; runIndex++) {
@@ -129,7 +131,7 @@ async function runLighthouse(port, runs) {
129
131
  const chromeDir = path.join(baseTmpDir, `chrome-${runNumber}`);
130
132
  fs.mkdirSync(chromeDir, { recursive: true });
131
133
  console.log(` [lh] Run ${runNumber}/${runs}`);
132
- const child = (0, node_child_process_1.spawn)("node", [runnerPath, `http://127.0.0.1:${port}/`, reportPath, chromeDir], {
134
+ const child = (0, node_child_process_1.spawn)("node", [runnerPath, fullUrl, reportPath, chromeDir], {
133
135
  shell: false,
134
136
  stdio: ["ignore", "pipe", "pipe"],
135
137
  env: {
@@ -196,3 +198,74 @@ async function runLighthouse(port, runs) {
196
198
  await removeDirWithRetries(baseTmpDir);
197
199
  }
198
200
  }
201
+ async function runLighthouse(port, runs, routePath = "/") {
202
+ const normalizedPath = routePath.startsWith("/") ? routePath : `/${routePath}`;
203
+ const label = normalizedPath === "/" ? "/" : normalizedPath;
204
+ return runLighthouseCore(`http://127.0.0.1:${port}${normalizedPath}`, runs, label);
205
+ }
206
+ /** Run Lighthouse against an arbitrary external URL (Pro: compare-env feature). */
207
+ async function runLighthouseOnUrl(fullUrl, runs) {
208
+ return runLighthouseCore(fullUrl, runs, fullUrl);
209
+ }
210
+ /**
211
+ * Run Lighthouse on multiple routes and aggregate results.
212
+ *
213
+ * Aggregation strategy (weighted average):
214
+ * - Root route "/" receives weight 2 (most visible page)
215
+ * - All other routes receive weight 1
216
+ *
217
+ * Gold eligibility: every route must individually pass all thresholds.
218
+ * Silver/Bronze eligibility: based on the weighted-average aggregate score.
219
+ */
220
+ async function runLighthouseOnRoutes(port, runs, routes) {
221
+ // Deduplicate and ensure "/" comes first
222
+ const seen = new Set();
223
+ const ordered = [];
224
+ for (const r of ["/", ...routes]) {
225
+ const normalized = r.startsWith("/") ? r : `/${r}`;
226
+ if (!seen.has(normalized)) {
227
+ seen.add(normalized);
228
+ ordered.push(normalized);
229
+ }
230
+ }
231
+ const perRoute = [];
232
+ for (const route of ordered) {
233
+ const result = await runLighthouse(port, runs, route);
234
+ perRoute.push({ route, scores: result.scores, errors: result.errors });
235
+ }
236
+ // Weighted average across routes that produced scores
237
+ const withScores = perRoute.filter((r) => r.scores !== null);
238
+ let aggregated = null;
239
+ if (withScores.length > 0) {
240
+ let totalWeight = 0;
241
+ let sumPerf = 0;
242
+ let sumA11y = 0;
243
+ let sumSeo = 0;
244
+ let sumBp = 0;
245
+ for (const r of withScores) {
246
+ const weight = r.route === "/" ? 2 : 1;
247
+ totalWeight += weight;
248
+ sumPerf += r.scores.performance * weight;
249
+ sumA11y += r.scores.accessibility * weight;
250
+ sumSeo += r.scores.seo * weight;
251
+ sumBp += r.scores.bestPractices * weight;
252
+ }
253
+ aggregated = {
254
+ performance: Math.round(sumPerf / totalWeight),
255
+ accessibility: Math.round(sumA11y / totalWeight),
256
+ seo: Math.round(sumSeo / totalWeight),
257
+ bestPractices: Math.round(sumBp / totalWeight),
258
+ };
259
+ console.log(` Lighthouse aggregate (weighted avg): P=${aggregated.performance} A=${aggregated.accessibility} S=${aggregated.seo} BP=${aggregated.bestPractices}`);
260
+ }
261
+ return {
262
+ aggregated,
263
+ perRoute,
264
+ allRoutesPass(thresholds) {
265
+ return withScores.every((r) => r.scores.performance >= thresholds.performance &&
266
+ r.scores.accessibility >= thresholds.accessibility &&
267
+ r.scores.seo >= thresholds.seo &&
268
+ r.scores.bestPractices >= thresholds.bestPractices);
269
+ },
270
+ };
271
+ }
@@ -0,0 +1,17 @@
1
+ export interface OutdatedPackage {
2
+ name: string;
3
+ current: string;
4
+ wanted: string;
5
+ latest: string;
6
+ severity: "major" | "minor" | "patch";
7
+ }
8
+ export interface OutdatedCheckResult {
9
+ passed: boolean;
10
+ totalChecked: number;
11
+ outdatedCount: number;
12
+ majorOutdated: number;
13
+ packages: OutdatedPackage[];
14
+ advisory: string;
15
+ skipped: boolean;
16
+ }
17
+ export declare function runOutdatedCheck(projectDir: string, timeoutMs?: number): Promise<OutdatedCheckResult>;
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runOutdatedCheck = runOutdatedCheck;
4
+ /**
5
+ * Outdated dependency checker.
6
+ *
7
+ * Runs `npm outdated --json` and reports packages that are behind
8
+ * their latest version. Advisory-only — does not block deployment.
9
+ */
10
+ const node_child_process_1 = require("node:child_process");
11
+ function classifySemverDiff(current, latest) {
12
+ const parseVer = (v) => {
13
+ const cleaned = v.replace(/^\^|~|>=?|v/g, "").split(".")[0] ?? "0";
14
+ return parseInt(cleaned, 10) || 0;
15
+ };
16
+ const parseMinor = (v) => {
17
+ const parts = v.replace(/^\^|~|>=?|v/g, "").split(".");
18
+ return parseInt(parts[1] ?? "0", 10) || 0;
19
+ };
20
+ const curMajor = parseVer(current);
21
+ const latMajor = parseVer(latest);
22
+ if (latMajor > curMajor)
23
+ return "major";
24
+ const curMinor = parseMinor(current);
25
+ const latMinor = parseMinor(latest);
26
+ if (latMinor > curMinor)
27
+ return "minor";
28
+ return "patch";
29
+ }
30
+ async function runOutdatedCheck(projectDir, timeoutMs = 30000) {
31
+ console.log(" Running outdated dependency check...");
32
+ return new Promise((resolve) => {
33
+ const chunks = [];
34
+ const proc = process.platform === "win32"
35
+ ? (0, node_child_process_1.spawn)(process.env.ComSpec || "cmd.exe", ["/d", "/c", "npm outdated --json"], { stdio: ["ignore", "pipe", "pipe"], cwd: projectDir, shell: false })
36
+ : (0, node_child_process_1.spawn)("npm", ["outdated", "--json"], {
37
+ shell: true,
38
+ stdio: ["ignore", "pipe", "pipe"],
39
+ cwd: projectDir,
40
+ });
41
+ const timer = setTimeout(() => {
42
+ try {
43
+ proc.kill();
44
+ }
45
+ catch { }
46
+ resolve({
47
+ passed: true,
48
+ totalChecked: 0,
49
+ outdatedCount: 0,
50
+ majorOutdated: 0,
51
+ packages: [],
52
+ advisory: "Outdated check timed out",
53
+ skipped: false,
54
+ });
55
+ }, timeoutMs);
56
+ proc.stdout?.on("data", (chunk) => chunks.push(chunk.toString()));
57
+ proc.stderr?.on("data", () => { });
58
+ proc.on("exit", (code) => {
59
+ clearTimeout(timer);
60
+ const output = chunks.join("");
61
+ // npm outdated exits with code 1 when outdated packages exist
62
+ // and code 0 when everything is up to date
63
+ if (code === 0 || !output.trim()) {
64
+ console.log(" Outdated: all packages up to date");
65
+ resolve({
66
+ passed: true,
67
+ totalChecked: 0,
68
+ outdatedCount: 0,
69
+ majorOutdated: 0,
70
+ packages: [],
71
+ advisory: "All packages up to date",
72
+ skipped: false,
73
+ });
74
+ return;
75
+ }
76
+ try {
77
+ const data = JSON.parse(output);
78
+ const packages = [];
79
+ for (const [name, info] of Object.entries(data)) {
80
+ if (!info.current || !info.latest)
81
+ continue;
82
+ const severity = classifySemverDiff(info.current, info.latest);
83
+ packages.push({
84
+ name,
85
+ current: info.current,
86
+ wanted: info.wanted ?? info.current,
87
+ latest: info.latest,
88
+ severity,
89
+ });
90
+ }
91
+ const majorOutdated = packages.filter((p) => p.severity === "major").length;
92
+ const outdatedCount = packages.length;
93
+ const advisory = majorOutdated > 0
94
+ ? `${majorOutdated} major version(s) behind latest; ${outdatedCount} total outdated`
95
+ : `${outdatedCount} package(s) behind latest (no major)`;
96
+ console.log(` Outdated: ${advisory}`);
97
+ for (const pkg of packages.filter((p) => p.severity === "major").slice(0, 5)) {
98
+ console.log(` ${pkg.name}: ${pkg.current} → ${pkg.latest} (${pkg.severity})`);
99
+ }
100
+ resolve({
101
+ passed: majorOutdated === 0,
102
+ totalChecked: 0,
103
+ outdatedCount,
104
+ majorOutdated,
105
+ packages,
106
+ advisory,
107
+ skipped: false,
108
+ });
109
+ }
110
+ catch {
111
+ resolve({
112
+ passed: true,
113
+ totalChecked: 0,
114
+ outdatedCount: 0,
115
+ majorOutdated: 0,
116
+ packages: [],
117
+ advisory: "Could not parse npm outdated output",
118
+ skipped: false,
119
+ });
120
+ }
121
+ });
122
+ });
123
+ }
@@ -2,6 +2,13 @@ import type { E2EScenarioResult } from "./e2e.js";
2
2
  import type { LighthouseScores } from "./grade.js";
3
3
  import type { TierVerificationView, VerificationReport } from "./verification-core/index.js";
4
4
  import { type VisualDiffResult } from "./visual-diff.js";
5
+ import type { TypecheckResult } from "./typecheck.js";
6
+ import type { SecretScanResult } from "./secret-scan.js";
7
+ import type { BundleSizeResult } from "./bundle-size.js";
8
+ import type { OutdatedCheckResult } from "./outdated-check.js";
9
+ import type { A11yDeepResult } from "./a11y-deep.js";
10
+ import type { SeoDeepResult } from "./seo-deep.js";
11
+ import type { VitalsBudgetResult } from "./vitals-budget.js";
5
12
  export interface MarkdownReportResult {
6
13
  grade: string;
7
14
  timestamp: string;
@@ -20,6 +27,13 @@ export interface MarkdownReportResult {
20
27
  runs: number;
21
28
  }) | null;
22
29
  visualDiff?: VisualDiffResult | null;
30
+ typecheck?: TypecheckResult | null;
31
+ secretScan?: SecretScanResult | null;
32
+ bundleSize?: BundleSizeResult | null;
33
+ outdatedCheck?: OutdatedCheckResult | null;
34
+ a11yDeep?: A11yDeepResult | null;
35
+ seoDeep?: SeoDeepResult | null;
36
+ vitalsBudget?: VitalsBudgetResult | null;
23
37
  thresholds: {
24
38
  performance: number;
25
39
  accessibility: number;
@@ -162,6 +162,27 @@ function renderMetrics(result) {
162
162
  if (result.visualDiff) {
163
163
  lines.push(`| Visual diff | ${(0, visual_diff_js_1.formatVisualDiffSummary)(result.visualDiff)} |`);
164
164
  }
165
+ if (result.typecheck && !result.typecheck.skipped) {
166
+ lines.push(`| TypeScript | ${result.typecheck.passed ? "OK" : `${result.typecheck.errorCount} error(s)`} |`);
167
+ }
168
+ if (result.secretScan && !result.secretScan.skipped) {
169
+ lines.push(`| Secret scan | ${result.secretScan.passed ? "OK" : `${result.secretScan.findings.length} finding(s)`} (${result.secretScan.filesScanned} files) |`);
170
+ }
171
+ if (result.bundleSize && !result.bundleSize.skipped) {
172
+ lines.push(`| Bundle size | ${result.bundleSize.advisory} |`);
173
+ }
174
+ if (result.outdatedCheck && !result.outdatedCheck.skipped) {
175
+ lines.push(`| Outdated deps | ${result.outdatedCheck.advisory} |`);
176
+ }
177
+ if (result.a11yDeep && !result.a11yDeep.skipped) {
178
+ lines.push(`| A11y deep | ${result.a11yDeep.summary} |`);
179
+ }
180
+ if (result.seoDeep && !result.seoDeep.skipped) {
181
+ lines.push(`| SEO deep | ${result.seoDeep.summary} |`);
182
+ }
183
+ if (result.vitalsBudget && !result.vitalsBudget.skipped) {
184
+ lines.push(`| Vitals budget | ${result.vitalsBudget.summary} |`);
185
+ }
165
186
  lines.push("");
166
187
  return `${lines.join("\n")}\n`;
167
188
  }
@@ -0,0 +1,7 @@
1
+ export interface RuntimeRouteDiscoveryResult {
2
+ routes: string[];
3
+ scriptUrls: string[];
4
+ }
5
+ export declare function extractScriptUrlsFromHtml(html: string, baseUrl: string): string[];
6
+ export declare function extractRoutesFromText(content: string): string[];
7
+ export declare function discoverRuntimeRoutes(baseUrl: string): Promise<RuntimeRouteDiscoveryResult>;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractScriptUrlsFromHtml = extractScriptUrlsFromHtml;
4
+ exports.extractRoutesFromText = extractRoutesFromText;
5
+ exports.discoverRuntimeRoutes = discoverRuntimeRoutes;
6
+ const SCRIPT_SRC_REGEX = /<script[^>]+src=["']([^"'#?]+(?:\?[^"'#]*)?)["']/gi;
7
+ const HTML_ROUTE_REGEX = /(?:href|data-href)=["'](\/[^"'#? ]*)/gi;
8
+ const ROUTE_SNIPPET_REGEXES = [
9
+ /(?:path|pathname|href|to|route|router\.push|navigate)\s*[:=(]\s*["'`]((?:\/(?!\/)[^"'`?#]+))["'`]/g,
10
+ /["'`]((?:\/(?!\/)[a-z0-9][^"'`?#]*))["'`]/gi,
11
+ ];
12
+ function normalizeRoute(candidate) {
13
+ const decoded = candidate
14
+ .replace(/\\u002F/gi, "/")
15
+ .replace(/\\\//g, "/")
16
+ .trim();
17
+ if (!decoded.startsWith("/"))
18
+ return null;
19
+ const normalized = decoded
20
+ .split("?")[0]
21
+ ?.split("#")[0]
22
+ ?.replace(/\/+/g, "/")
23
+ ?.replace(/\/$/, "") || "/";
24
+ if (normalized === "/" || normalized.length < 2)
25
+ return null;
26
+ if (normalized.startsWith("/_next/") || normalized.startsWith("/api/"))
27
+ return null;
28
+ if (/[.*:[\]]/.test(normalized))
29
+ return null;
30
+ if (/\.[a-z0-9]{2,8}$/i.test(normalized))
31
+ return null;
32
+ if (/\s/.test(normalized))
33
+ return null;
34
+ if (!/^\/[a-z0-9/_-]+$/i.test(normalized))
35
+ return null;
36
+ return normalized;
37
+ }
38
+ function extractScriptUrlsFromHtml(html, baseUrl) {
39
+ const urls = [];
40
+ const seen = new Set();
41
+ for (const match of html.matchAll(SCRIPT_SRC_REGEX)) {
42
+ const raw = match[1];
43
+ if (!raw)
44
+ continue;
45
+ try {
46
+ const scriptUrl = new URL(raw, baseUrl);
47
+ if (scriptUrl.origin !== new URL(baseUrl).origin)
48
+ continue;
49
+ const href = scriptUrl.href;
50
+ if (!seen.has(href)) {
51
+ seen.add(href);
52
+ urls.push(href);
53
+ }
54
+ }
55
+ catch {
56
+ // Ignore malformed URLs.
57
+ }
58
+ }
59
+ return urls;
60
+ }
61
+ function extractRoutesFromText(content) {
62
+ const routes = [];
63
+ const seen = new Set();
64
+ for (const regex of ROUTE_SNIPPET_REGEXES) {
65
+ for (const match of content.matchAll(regex)) {
66
+ const route = normalizeRoute(match[1] ?? "");
67
+ if (!route || seen.has(route))
68
+ continue;
69
+ seen.add(route);
70
+ routes.push(route);
71
+ }
72
+ }
73
+ return routes;
74
+ }
75
+ async function discoverRuntimeRoutes(baseUrl) {
76
+ const htmlRes = await fetch(baseUrl, {
77
+ signal: AbortSignal.timeout(8000),
78
+ headers: { accept: "text/html,application/xhtml+xml" },
79
+ });
80
+ const html = await htmlRes.text();
81
+ const routes = new Set(extractRoutesFromText(html));
82
+ for (const match of html.matchAll(HTML_ROUTE_REGEX)) {
83
+ const route = normalizeRoute(match[1] ?? "");
84
+ if (route)
85
+ routes.add(route);
86
+ }
87
+ const scriptUrls = extractScriptUrlsFromHtml(html, baseUrl)
88
+ .filter((url) => /\/_next\/static\/chunks\/|assets\/|static\/|build\//i.test(url))
89
+ .slice(0, 10);
90
+ await Promise.all(scriptUrls.map(async (scriptUrl) => {
91
+ try {
92
+ const res = await fetch(scriptUrl, { signal: AbortSignal.timeout(5000) });
93
+ if (!res.ok)
94
+ return;
95
+ const content = await res.text();
96
+ for (const route of extractRoutesFromText(content)) {
97
+ routes.add(route);
98
+ }
99
+ }
100
+ catch {
101
+ // Skip chunk fetch failures. This is best-effort coverage expansion.
102
+ }
103
+ }));
104
+ return {
105
+ routes: Array.from(routes).sort((a, b) => a.localeCompare(b)),
106
+ scriptUrls,
107
+ };
108
+ }
@@ -0,0 +1,15 @@
1
+ export interface SecretScanResult {
2
+ passed: boolean;
3
+ findings: SecretFinding[];
4
+ filesScanned: number;
5
+ skipped: boolean;
6
+ }
7
+ export interface SecretFinding {
8
+ file: string;
9
+ line: number;
10
+ pattern: string;
11
+ preview: string;
12
+ }
13
+ export declare function runSecretScan(projectDir: string, options?: {
14
+ ignorePaths?: string[];
15
+ }): Promise<SecretScanResult>;
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runSecretScan = runSecretScan;
37
+ /**
38
+ * Secret/credential leak scanner.
39
+ *
40
+ * Scans source files for hardcoded secrets using regex patterns.
41
+ * Supports ignore paths and custom ignore patterns from .laxy.yml.
42
+ */
43
+ const fs = __importStar(require("node:fs"));
44
+ const path = __importStar(require("node:path"));
45
+ const DEFAULT_IGNORE_DIRS = new Set([
46
+ "node_modules",
47
+ ".git",
48
+ ".next",
49
+ "dist",
50
+ "build",
51
+ ".laxy-tmp",
52
+ ".laxy-baselines",
53
+ "coverage",
54
+ ".cache",
55
+ "test",
56
+ "tests",
57
+ "__tests__",
58
+ "spec",
59
+ ]);
60
+ const SECRET_PATTERNS = [
61
+ { name: "AWS Access Key", regex: /AKIA[0-9A-Z]{16}/ },
62
+ { name: "AWS Secret Key", regex: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*['"][A-Za-z0-9/+=]{40}['"]/ },
63
+ { name: "GitHub Token", regex: /ghp_[A-Za-z0-9]{36}/ },
64
+ { name: "GitHub OAuth", regex: /gho_[A-Za-z0-9]{36}/ },
65
+ { name: "GitHub App Token", regex: /(?:ghs_|ghu_)[A-Za-z0-9]{36}/ },
66
+ { name: "Slack Token", regex: /xox[baprs]-[A-Za-z0-9-]{10,}/ },
67
+ { name: "Slack Webhook", regex: /hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/ },
68
+ { name: "Private Key Block", regex: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/ },
69
+ { name: "Google API Key", regex: /AIza[0-9A-Za-z_-]{35}/ },
70
+ { name: "Google OAuth Token", regex: /[0-9]+-[a-z0-9_]{32}\.apps\.googleusercontent\.com/ },
71
+ { name: "Stripe Secret Key", regex: /sk_live_[0-9a-zA-Z]{24,}/ },
72
+ { name: "Stripe Publishable Key", regex: /pk_live_[0-9a-zA-Z]{24,}/ },
73
+ { name: "Twilio API Key", regex: /SK[0-9a-fA-F]{32}/ },
74
+ { name: "SendGrid API Key", regex: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/ },
75
+ { name: "Mailgun API Key", regex: /key-[0-9a-zA-Z]{32}/ },
76
+ { name: "Hardcoded Bearer Token", regex: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/ },
77
+ { name: "JWT-like Secret", regex: /['"]eyJ[A-Za-z0-9\-._~+/]+=*\.[A-Za-z0-9\-._~+/]+=*['"]/ },
78
+ { name: "Generic Secret Assignment", regex: /(?:secret|password|passwd|token|api_key|apikey|access_key|private_key)\s*[=:]\s*['"][^'"]{8,}['"]/i },
79
+ ];
80
+ const ALLOWLIST_PATTERNS = [
81
+ // Common false positives
82
+ /process\.env\./,
83
+ /import\.meta\.env\./,
84
+ /NEXT_PUBLIC_/,
85
+ /VITE_/,
86
+ /REACT_APP_/,
87
+ /NUXT_ENV_/,
88
+ /placeholder/i,
89
+ /example/i,
90
+ /your[_-]?(?:api|secret|key|token)/i,
91
+ /xxx+/,
92
+ /<[^>]+>/,
93
+ /\$\{\{.*\}\}/, // GitHub Actions / CI template variables
94
+ /secrets\.[A-Z_]+/, // GitHub Actions secrets references
95
+ /env\.[A-Z_]+/, // CI env variable references
96
+ /regex:\s*\//, // Regex pattern definitions in source code
97
+ /^\s*\/\//, // Single-line comments (// ...)
98
+ /^\s*\*/, // Block comment lines (* ...)
99
+ /^\s*<!--/, // HTML comment lines
100
+ ];
101
+ function shouldIgnoreLine(line) {
102
+ return ALLOWLIST_PATTERNS.some((p) => p.test(line));
103
+ }
104
+ /**
105
+ * Masks potential secret values in a line preview so that
106
+ * actual credentials are never exposed in JSON output or reports.
107
+ * Replaces quoted string contents after = or : with ***.
108
+ */
109
+ function maskSecret(line) {
110
+ return line
111
+ // Mask single-quoted values: = '...' or : '...'
112
+ .replace(/([=:])\s*'([^']{4,})'/g, "$1 '***'")
113
+ // Mask double-quoted values: = "..." or : "..."
114
+ .replace(/([=:])\s*"([^"]{4,})"/g, '$1 "***"')
115
+ // Mask bare tokens after Bearer
116
+ .replace(/(Bearer\s+)[A-Za-z0-9\-._~+/]+=*/g, "$1***")
117
+ // Mask AKIA keys
118
+ .replace(/(AKIA)[0-9A-Z]{16}/g, "$1****************")
119
+ // Mask ghp_/gho_/ghs_/ghu_ tokens
120
+ .replace(/(gh[pous]_)[A-Za-z0-9]{36}/g, "$1************************************")
121
+ // Mask xoxb-/xoxp- etc tokens
122
+ .replace(/(xox[baprs]-)[A-Za-z0-9-]{10,}/g, "$1**********")
123
+ // Mask AIza keys
124
+ .replace(/(AIza)[0-9A-Za-z_-]{35}/g, "$1***********************************")
125
+ // Mask sk_live_/pk_live_ keys
126
+ .replace(/(s?k_live_)[0-9a-zA-Z]{24,}/g, "$1************************")
127
+ // Mask SK hex keys (Twilio)
128
+ .replace(/(SK)[0-9a-fA-F]{32}/g, "$1********************************")
129
+ // Mask key- prefix (Mailgun)
130
+ .replace(/(key-)[0-9a-zA-Z]{32}/g, "$1********************************")
131
+ // Mask SG. tokens (SendGrid)
132
+ .replace(/(SG\.)[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g, "$1**********************.*******************************************");
133
+ }
134
+ function scanFile(filePath, relativePath, ignorePatterns) {
135
+ if (ignorePatterns.some((p) => relativePath.includes(p)))
136
+ return [];
137
+ const findings = [];
138
+ let content;
139
+ try {
140
+ content = fs.readFileSync(filePath, "utf-8");
141
+ }
142
+ catch {
143
+ return [];
144
+ }
145
+ const lines = content.split("\n");
146
+ for (let i = 0; i < lines.length; i++) {
147
+ const line = lines[i];
148
+ if (shouldIgnoreLine(line))
149
+ continue;
150
+ for (const { name, regex } of SECRET_PATTERNS) {
151
+ if (regex.test(line)) {
152
+ const preview = maskSecret(line.trim().slice(0, 120));
153
+ findings.push({
154
+ file: relativePath,
155
+ line: i + 1,
156
+ pattern: name,
157
+ preview,
158
+ });
159
+ break; // one finding per line max
160
+ }
161
+ }
162
+ }
163
+ return findings;
164
+ }
165
+ function walkDir(dir, rootDir, ignoreDirs, ignorePatterns) {
166
+ const results = [];
167
+ let entries;
168
+ try {
169
+ entries = fs.readdirSync(dir, { withFileTypes: true });
170
+ }
171
+ catch {
172
+ return results;
173
+ }
174
+ for (const entry of entries) {
175
+ if (ignoreDirs.has(entry.name))
176
+ continue;
177
+ if (entry.name.startsWith(".") && entry.name !== ".env")
178
+ continue;
179
+ const fullPath = path.join(dir, entry.name);
180
+ const relativePath = path.relative(rootDir, fullPath).replace(/\\/g, "/");
181
+ if (ignorePatterns.some((p) => relativePath.includes(p)))
182
+ continue;
183
+ if (entry.isDirectory()) {
184
+ results.push(...walkDir(fullPath, rootDir, ignoreDirs, ignorePatterns));
185
+ }
186
+ else if (entry.isFile()) {
187
+ const ext = path.extname(entry.name).toLowerCase();
188
+ if ([".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte", ".env", ".json", ".yml", ".yaml", ".toml", ".py", ".rb"].includes(ext) ||
189
+ entry.name === ".env" || entry.name.startsWith(".env.")) {
190
+ results.push(fullPath);
191
+ }
192
+ }
193
+ }
194
+ return results;
195
+ }
196
+ async function runSecretScan(projectDir, options) {
197
+ const ignorePatterns = options?.ignorePaths ?? [];
198
+ const ignoreDirs = new Set(DEFAULT_IGNORE_DIRS);
199
+ console.log(" Running secret scan...");
200
+ const files = walkDir(projectDir, projectDir, ignoreDirs, ignorePatterns);
201
+ const allFindings = [];
202
+ for (const file of files) {
203
+ const relativePath = path.relative(projectDir, file).replace(/\\/g, "/");
204
+ const findings = scanFile(file, relativePath, ignorePatterns);
205
+ allFindings.push(...findings);
206
+ }
207
+ const passed = allFindings.length === 0;
208
+ if (passed) {
209
+ console.log(` Secret scan: OK (${files.length} files scanned)`);
210
+ }
211
+ else {
212
+ console.log(` Secret scan: ${allFindings.length} finding(s) in ${files.length} files`);
213
+ for (const f of allFindings.slice(0, 5)) {
214
+ console.error(` ${f.file}:${f.line} [${f.pattern}]`);
215
+ }
216
+ }
217
+ return { passed, findings: allFindings, filesScanned: files.length, skipped: false };
218
+ }
@@ -5,5 +5,13 @@ export interface SecurityAuditResult {
5
5
  moderate: number;
6
6
  low: number;
7
7
  summary: string;
8
+ missingHeaders: string[];
9
+ headerCheckUrl?: string;
10
+ headerCheckError?: string;
8
11
  }
9
- export declare function runSecurityAudit(cwd: string, timeoutMs?: number): Promise<SecurityAuditResult>;
12
+ export declare function auditSecurityHeaders(url: string): Promise<{
13
+ missingHeaders: string[];
14
+ checkedUrl: string;
15
+ error?: string;
16
+ }>;
17
+ export declare function runSecurityAudit(cwd: string, appUrl?: string, timeoutMs?: number): Promise<SecurityAuditResult>;