laxy-verify 1.2.3 → 1.3.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 +23 -0
- package/dist/audit/broken-links.d.ts +25 -25
- package/dist/audit/broken-links.js +97 -97
- package/dist/badge.d.ts +2 -1
- package/dist/badge.js +18 -14
- package/dist/cli.js +1246 -1233
- package/dist/config.d.ts +102 -102
- package/dist/config.js +360 -360
- package/dist/entitlement.d.ts +15 -13
- package/dist/entitlement.js +98 -94
- package/dist/init.js +132 -132
- package/dist/lighthouse.d.ts +37 -37
- package/dist/lighthouse.js +231 -231
- package/dist/report-markdown.d.ts +53 -53
- package/dist/report-markdown.js +407 -407
- package/dist/security-audit.d.ts +17 -17
- package/dist/security-audit.js +127 -127
- package/dist/verification-core/report.js +526 -526
- package/dist/verification-core/types.d.ts +164 -164
- package/dist/visual-diff.d.ts +33 -33
- package/dist/visual-diff.js +213 -213
- package/package.json +1 -1
- package/dist/ai-analysis.d.ts +0 -28
- package/dist/ai-analysis.js +0 -32
- package/dist/compare-env.d.ts +0 -23
- package/dist/compare-env.js +0 -55
- package/dist/init-analysis.d.ts +0 -6
- package/dist/init-analysis.js +0 -302
- package/dist/route-discovery.d.ts +0 -7
- package/dist/route-discovery.js +0 -108
package/README.md
CHANGED
|
@@ -188,6 +188,16 @@ Passed:
|
|
|
188
188
|
Artifacts:
|
|
189
189
|
- .laxy-result.json
|
|
190
190
|
- laxy-verify-report.md
|
|
191
|
+
|
|
192
|
+
Badge (auto-updates with each run):
|
|
193
|
+
[](https://laxy.app)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
For Free accounts, the CLI prints a tip instead:
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
Tip: Pro tracks your last 30 runs so you can see if Performance or Grade is improving.
|
|
200
|
+
https://laxy.app/pricing
|
|
191
201
|
```
|
|
192
202
|
|
|
193
203
|
## The decision it helps you make
|
|
@@ -318,9 +328,13 @@ Pro and Team accounts unlock additional capabilities on top of the same verifica
|
|
|
318
328
|
- **Result saving and sharing** — `--share` saves the run to your dashboard and returns a shareable URL
|
|
319
329
|
- **Environment comparison** — `--compare <url>` runs Lighthouse against a reference URL (e.g. staging or production) and shows score deltas between your local build and the reference
|
|
320
330
|
- **AI failure analysis** — when a run ends in `hold`, Claude analyzes the failure context and returns a root cause summary with top fix suggestions
|
|
331
|
+
- **History trend tracking** — the last 30 runs are stored so you can see whether your grade and performance scores are improving or regressing over time
|
|
332
|
+
- **Dynamic README badge** — after each run, the CLI prints a Markdown badge snippet that links to your live verification status. The badge auto-updates with every run so your README always reflects current grade
|
|
321
333
|
- **Team Slack / Discord alerts** — grade drops and `hold` verdicts fire webhook notifications with score deltas and blocker details
|
|
322
334
|
- **Weekly team report** — automated weekly summary of verification activity across your team's repos
|
|
323
335
|
|
|
336
|
+
Free accounts see a hint after each run pointing to the history trend feature.
|
|
337
|
+
|
|
324
338
|
## Secret scan patterns
|
|
325
339
|
|
|
326
340
|
When `--secret-scan` is enabled, the following patterns are detected:
|
|
@@ -418,6 +432,15 @@ It is a pre-merge and pre-release verification layer, not your entire quality sy
|
|
|
418
432
|
|
|
419
433
|
## Changelog
|
|
420
434
|
|
|
435
|
+
### v1.3.0 — Pro history trend, dynamic badge, CLI nudge
|
|
436
|
+
|
|
437
|
+
- **History trend tracking** — Pro accounts now store the last 30 verification runs. Grade and performance scores accumulate so you can see whether your app is improving or regressing over time
|
|
438
|
+
- **Dynamic README badge** — after each run, Pro accounts see a Markdown badge snippet in the CLI output. The badge auto-updates with every run via the `/api/badge/:id` endpoint
|
|
439
|
+
- **CLI nudge** — Free accounts see a one-line tip after each run pointing to the history trend feature
|
|
440
|
+
- Added `generateDynamicBadgeMarkdown(repoId, apiUrl)` export to the `badge` module
|
|
441
|
+
- Added `share_result` and `history_trend` flags to `EntitlementFeatures` interface
|
|
442
|
+
- 17 test files, 88 unit tests
|
|
443
|
+
|
|
421
444
|
### v1.2.3 — Bug fix
|
|
422
445
|
|
|
423
446
|
- Fix E2E and visual diff connection failure on Windows with Node.js 17+: `verifyUrl` now uses `localhost` instead of `127.0.0.1`. On Windows, Vite binds to `::1` (IPv6) which does not accept IPv4 connections.
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Broken-links audit.
|
|
3
|
-
* Uses the crawl result to find all internal links, then validates each
|
|
4
|
-
* with an HTTP HEAD/GET request. Links that return 4xx/5xx or timeout are
|
|
5
|
-
* reported as blockers.
|
|
6
|
-
*/
|
|
7
|
-
import { type CrawlResult } from "../crawler.js";
|
|
8
|
-
export interface BrokenLink {
|
|
9
|
-
url: string;
|
|
10
|
-
path: string;
|
|
11
|
-
status: number;
|
|
12
|
-
statusText: string;
|
|
13
|
-
severity: "critical" | "high";
|
|
14
|
-
}
|
|
15
|
-
export interface BrokenLinksResult {
|
|
16
|
-
brokenLinks: BrokenLink[];
|
|
17
|
-
checkedCount: number;
|
|
18
|
-
hasBrokenLinks: boolean;
|
|
19
|
-
summary: string;
|
|
20
|
-
}
|
|
21
|
-
export interface BrokenLinksAuditOptions {
|
|
22
|
-
extraRoutes?: string[];
|
|
23
|
-
abortSignal?: AbortSignal;
|
|
24
|
-
}
|
|
25
|
-
export declare function auditBrokenLinks(crawlResult: CrawlResult | undefined, baseUrl: string, options?: BrokenLinksAuditOptions): Promise<BrokenLinksResult>;
|
|
1
|
+
/**
|
|
2
|
+
* Broken-links audit.
|
|
3
|
+
* Uses the crawl result to find all internal links, then validates each
|
|
4
|
+
* with an HTTP HEAD/GET request. Links that return 4xx/5xx or timeout are
|
|
5
|
+
* reported as blockers.
|
|
6
|
+
*/
|
|
7
|
+
import { type CrawlResult } from "../crawler.js";
|
|
8
|
+
export interface BrokenLink {
|
|
9
|
+
url: string;
|
|
10
|
+
path: string;
|
|
11
|
+
status: number;
|
|
12
|
+
statusText: string;
|
|
13
|
+
severity: "critical" | "high";
|
|
14
|
+
}
|
|
15
|
+
export interface BrokenLinksResult {
|
|
16
|
+
brokenLinks: BrokenLink[];
|
|
17
|
+
checkedCount: number;
|
|
18
|
+
hasBrokenLinks: boolean;
|
|
19
|
+
summary: string;
|
|
20
|
+
}
|
|
21
|
+
export interface BrokenLinksAuditOptions {
|
|
22
|
+
extraRoutes?: string[];
|
|
23
|
+
abortSignal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
export declare function auditBrokenLinks(crawlResult: CrawlResult | undefined, baseUrl: string, options?: BrokenLinksAuditOptions): Promise<BrokenLinksResult>;
|
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.auditBrokenLinks = auditBrokenLinks;
|
|
4
|
-
const TIMEOUT_MS = 5000;
|
|
5
|
-
const VALID_OK_STATUS = [200, 201, 202, 203, 204, 301, 302, 303, 307, 308];
|
|
6
|
-
function isSuccessStatus(n) {
|
|
7
|
-
return VALID_OK_STATUS.includes(n);
|
|
8
|
-
}
|
|
9
|
-
async function auditBrokenLinks(crawlResult, baseUrl, options = {}) {
|
|
10
|
-
const allUrls = [];
|
|
11
|
-
if (crawlResult) {
|
|
12
|
-
for (const page of crawlResult.pages) {
|
|
13
|
-
for (const href of page.internalLinks) {
|
|
14
|
-
try {
|
|
15
|
-
const url = new URL(href, baseUrl).href;
|
|
16
|
-
if (!allUrls.includes(url))
|
|
17
|
-
allUrls.push(url);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
// skip malformed URLs
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
for (const route of options.extraRoutes ?? []) {
|
|
26
|
-
try {
|
|
27
|
-
const url = new URL(route, baseUrl).href;
|
|
28
|
-
if (!allUrls.includes(url))
|
|
29
|
-
allUrls.push(url);
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
// skip malformed routes
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
const uniqueUrls = allUrls;
|
|
36
|
-
const brokenLinks = [];
|
|
37
|
-
await Promise.all(uniqueUrls.map(async (url) => {
|
|
38
|
-
if (options.abortSignal?.aborted)
|
|
39
|
-
return;
|
|
40
|
-
try {
|
|
41
|
-
const controller = new AbortController();
|
|
42
|
-
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
43
|
-
let status = 0;
|
|
44
|
-
let statusText = "";
|
|
45
|
-
try {
|
|
46
|
-
const res = await fetch(url, {
|
|
47
|
-
method: "HEAD",
|
|
48
|
-
redirect: "follow",
|
|
49
|
-
signal: controller.signal,
|
|
50
|
-
});
|
|
51
|
-
status = res.status;
|
|
52
|
-
statusText = res.statusText;
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// Fall back to GET if HEAD is not allowed
|
|
56
|
-
const controller2 = new AbortController();
|
|
57
|
-
const timer2 = setTimeout(() => controller2.abort(), TIMEOUT_MS);
|
|
58
|
-
try {
|
|
59
|
-
const res = await fetch(url, {
|
|
60
|
-
method: "GET",
|
|
61
|
-
redirect: "follow",
|
|
62
|
-
signal: controller2.signal,
|
|
63
|
-
});
|
|
64
|
-
status = res.status;
|
|
65
|
-
statusText = res.statusText;
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
status = 0;
|
|
69
|
-
statusText = "timeout or network error";
|
|
70
|
-
}
|
|
71
|
-
clearTimeout(timer2);
|
|
72
|
-
}
|
|
73
|
-
clearTimeout(timer);
|
|
74
|
-
if (!isSuccessStatus(status)) {
|
|
75
|
-
const severity = status >= 500 ? "critical" : "high";
|
|
76
|
-
let path = url;
|
|
77
|
-
try {
|
|
78
|
-
path = new URL(url).pathname;
|
|
79
|
-
}
|
|
80
|
-
catch { }
|
|
81
|
-
brokenLinks.push({ url, path, status, statusText, severity });
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
// skip
|
|
86
|
-
}
|
|
87
|
-
}));
|
|
88
|
-
const summary = brokenLinks.length === 0
|
|
89
|
-
? `All ${uniqueUrls.length} links OK`
|
|
90
|
-
: `${brokenLinks.length} broken link(s) found`;
|
|
91
|
-
return {
|
|
92
|
-
brokenLinks,
|
|
93
|
-
checkedCount: uniqueUrls.length,
|
|
94
|
-
hasBrokenLinks: brokenLinks.length > 0,
|
|
95
|
-
summary,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.auditBrokenLinks = auditBrokenLinks;
|
|
4
|
+
const TIMEOUT_MS = 5000;
|
|
5
|
+
const VALID_OK_STATUS = [200, 201, 202, 203, 204, 301, 302, 303, 307, 308];
|
|
6
|
+
function isSuccessStatus(n) {
|
|
7
|
+
return VALID_OK_STATUS.includes(n);
|
|
8
|
+
}
|
|
9
|
+
async function auditBrokenLinks(crawlResult, baseUrl, options = {}) {
|
|
10
|
+
const allUrls = [];
|
|
11
|
+
if (crawlResult) {
|
|
12
|
+
for (const page of crawlResult.pages) {
|
|
13
|
+
for (const href of page.internalLinks) {
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(href, baseUrl).href;
|
|
16
|
+
if (!allUrls.includes(url))
|
|
17
|
+
allUrls.push(url);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// skip malformed URLs
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const route of options.extraRoutes ?? []) {
|
|
26
|
+
try {
|
|
27
|
+
const url = new URL(route, baseUrl).href;
|
|
28
|
+
if (!allUrls.includes(url))
|
|
29
|
+
allUrls.push(url);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// skip malformed routes
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const uniqueUrls = allUrls;
|
|
36
|
+
const brokenLinks = [];
|
|
37
|
+
await Promise.all(uniqueUrls.map(async (url) => {
|
|
38
|
+
if (options.abortSignal?.aborted)
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
43
|
+
let status = 0;
|
|
44
|
+
let statusText = "";
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(url, {
|
|
47
|
+
method: "HEAD",
|
|
48
|
+
redirect: "follow",
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
});
|
|
51
|
+
status = res.status;
|
|
52
|
+
statusText = res.statusText;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Fall back to GET if HEAD is not allowed
|
|
56
|
+
const controller2 = new AbortController();
|
|
57
|
+
const timer2 = setTimeout(() => controller2.abort(), TIMEOUT_MS);
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(url, {
|
|
60
|
+
method: "GET",
|
|
61
|
+
redirect: "follow",
|
|
62
|
+
signal: controller2.signal,
|
|
63
|
+
});
|
|
64
|
+
status = res.status;
|
|
65
|
+
statusText = res.statusText;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
status = 0;
|
|
69
|
+
statusText = "timeout or network error";
|
|
70
|
+
}
|
|
71
|
+
clearTimeout(timer2);
|
|
72
|
+
}
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
if (!isSuccessStatus(status)) {
|
|
75
|
+
const severity = status >= 500 ? "critical" : "high";
|
|
76
|
+
let path = url;
|
|
77
|
+
try {
|
|
78
|
+
path = new URL(url).pathname;
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
brokenLinks.push({ url, path, status, statusText, severity });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// skip
|
|
86
|
+
}
|
|
87
|
+
}));
|
|
88
|
+
const summary = brokenLinks.length === 0
|
|
89
|
+
? `All ${uniqueUrls.length} links OK`
|
|
90
|
+
: `${brokenLinks.length} broken link(s) found`;
|
|
91
|
+
return {
|
|
92
|
+
brokenLinks,
|
|
93
|
+
checkedCount: uniqueUrls.length,
|
|
94
|
+
hasBrokenLinks: brokenLinks.length > 0,
|
|
95
|
+
summary,
|
|
96
|
+
};
|
|
97
|
+
}
|
package/dist/badge.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export declare function generateBadge(grade: string): string;
|
|
1
|
+
export declare function generateBadge(grade: string): string;
|
|
2
|
+
export declare function generateDynamicBadgeMarkdown(repoId: string, apiUrl: string): string;
|
package/dist/badge.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.generateBadge = generateBadge;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateBadge = generateBadge;
|
|
4
|
+
exports.generateDynamicBadgeMarkdown = generateDynamicBadgeMarkdown;
|
|
5
|
+
function generateBadge(grade) {
|
|
6
|
+
const gradeLower = grade.toLowerCase();
|
|
7
|
+
const colors = {
|
|
8
|
+
gold: "yellow",
|
|
9
|
+
silver: "brightgreen",
|
|
10
|
+
bronze: "blue",
|
|
11
|
+
unverified: "lightgrey",
|
|
12
|
+
};
|
|
13
|
+
const color = colors[gradeLower] ?? "lightgrey";
|
|
14
|
+
return ``;
|
|
15
|
+
}
|
|
16
|
+
function generateDynamicBadgeMarkdown(repoId, apiUrl) {
|
|
17
|
+
return `[](${apiUrl})`;
|
|
18
|
+
}
|