@reddoorla/maintenance 0.49.0 → 0.50.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/dist/cli/bin.js +624 -48
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +3 -5
- package/dist/cli/commands/audit.js +434 -14
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.d.ts +84 -2
- package/dist/index.js +532 -32
- package/dist/index.js.map +1 -1
- package/dist/recipes/sync-configs.d.ts +1 -1
- package/dist/{types-DeKpgkG-.d.ts → types-QG-QhCYh.d.ts} +1 -1
- package/package.json +1 -1
package/dist/cli/bin.js
CHANGED
|
@@ -71,6 +71,16 @@ function isHttpUrl(s) {
|
|
|
71
71
|
}
|
|
72
72
|
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
73
73
|
}
|
|
74
|
+
function isNetlifyAppUrl(s) {
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = new URL(s);
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const host = parsed.hostname.toLowerCase();
|
|
82
|
+
return host === "netlify.app" || host.endsWith(".netlify.app");
|
|
83
|
+
}
|
|
74
84
|
var init_url = __esm({
|
|
75
85
|
"src/util/url.ts"() {
|
|
76
86
|
"use strict";
|
|
@@ -198,6 +208,14 @@ function mapRow(rec) {
|
|
|
198
208
|
securityVulnsHigh: f["Security Vulns High"] ?? null,
|
|
199
209
|
securityVulnsModerate: f["Security Vulns Moderate"] ?? null,
|
|
200
210
|
securityVulnsLow: f["Security Vulns Low"] ?? null,
|
|
211
|
+
lastSecurityAuditAt: f["Last security audit at"] ?? null,
|
|
212
|
+
certDaysRemaining: f["Cert days remaining"] ?? null,
|
|
213
|
+
domainCheckedAt: f["Domain checked at"] ?? null,
|
|
214
|
+
crossbrowserOk: typeof f["Crossbrowser OK"] === "boolean" ? f["Crossbrowser OK"] : null,
|
|
215
|
+
mobileOk: typeof f["Mobile OK"] === "boolean" ? f["Mobile OK"] : null,
|
|
216
|
+
linksOk: typeof f["Links OK"] === "boolean" ? f["Links OK"] : null,
|
|
217
|
+
brokenLinks: typeof f["Broken links"] === "number" ? f["Broken links"] : null,
|
|
218
|
+
browserCheckedAt: f["Browser checked at"] ?? null,
|
|
201
219
|
copyIntro: trimToNull(f["Copy \u2014 Intro"]),
|
|
202
220
|
copyContact: trimToNull(f["Copy \u2014 Contact"]),
|
|
203
221
|
copyFooter: trimToNull(f["Copy \u2014 Footer"]),
|
|
@@ -259,7 +277,24 @@ function securityFields(counts) {
|
|
|
259
277
|
"Security Vulns Critical": counts.critical,
|
|
260
278
|
"Security Vulns High": counts.high,
|
|
261
279
|
"Security Vulns Moderate": counts.moderate,
|
|
262
|
-
"Security Vulns Low": counts.low
|
|
280
|
+
"Security Vulns Low": counts.low,
|
|
281
|
+
// Stamp freshness alongside the counts so the Security Updates auto-tick can require a recent
|
|
282
|
+
// audit (a clean count from months ago must not silently keep ticking the box).
|
|
283
|
+
"Last security audit at": (/* @__PURE__ */ new Date()).toISOString()
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function domainFields(result) {
|
|
287
|
+
const fields = { "Domain checked at": result.checkedAt };
|
|
288
|
+
if (result.certDaysRemaining !== null) fields["Cert days remaining"] = result.certDaysRemaining;
|
|
289
|
+
return fields;
|
|
290
|
+
}
|
|
291
|
+
function browserFields(r) {
|
|
292
|
+
return {
|
|
293
|
+
"Crossbrowser OK": r.desktopOk,
|
|
294
|
+
"Mobile OK": r.mobileOk,
|
|
295
|
+
"Links OK": r.linksOk,
|
|
296
|
+
"Broken links": r.brokenLinks,
|
|
297
|
+
"Browser checked at": r.checkedAt
|
|
263
298
|
};
|
|
264
299
|
}
|
|
265
300
|
async function updateScores(base, recordId, scores) {
|
|
@@ -280,6 +315,8 @@ async function updateAuditFields(base, recordId, audits) {
|
|
|
280
315
|
if (audits.a11y) Object.assign(fields, a11yFields(audits.a11y));
|
|
281
316
|
if (audits.deps) Object.assign(fields, depsFields(audits.deps));
|
|
282
317
|
if (audits.security) Object.assign(fields, securityFields(audits.security));
|
|
318
|
+
if (audits.domain) Object.assign(fields, domainFields(audits.domain));
|
|
319
|
+
if (audits.browser) Object.assign(fields, browserFields(audits.browser));
|
|
283
320
|
await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
|
|
284
321
|
return fields;
|
|
285
322
|
}
|
|
@@ -474,6 +511,53 @@ var init_security_airtable = __esm({
|
|
|
474
511
|
}
|
|
475
512
|
});
|
|
476
513
|
|
|
514
|
+
// src/audits/domain-airtable.ts
|
|
515
|
+
function hasDomainResult(result) {
|
|
516
|
+
if (result.audit !== "domain") return false;
|
|
517
|
+
const d = result.details;
|
|
518
|
+
return !!d && typeof d.checkedAt === "string";
|
|
519
|
+
}
|
|
520
|
+
function domainResultFromAudit(result) {
|
|
521
|
+
if (result.audit !== "domain") {
|
|
522
|
+
throw new Error(`Expected a 'domain' AuditResult, got '${result.audit}'`);
|
|
523
|
+
}
|
|
524
|
+
const d = result.details;
|
|
525
|
+
return {
|
|
526
|
+
certDaysRemaining: typeof d?.certDaysRemaining === "number" ? d.certDaysRemaining : null,
|
|
527
|
+
checkedAt: typeof d?.checkedAt === "string" ? d.checkedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
var init_domain_airtable = __esm({
|
|
531
|
+
"src/audits/domain-airtable.ts"() {
|
|
532
|
+
"use strict";
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// src/audits/browser-airtable.ts
|
|
537
|
+
function hasBrowserResult(result) {
|
|
538
|
+
if (result.audit !== "browser") return false;
|
|
539
|
+
const d = result.details;
|
|
540
|
+
return !!d && typeof d.checkedAt === "string";
|
|
541
|
+
}
|
|
542
|
+
function browserFieldsFromAudit(result) {
|
|
543
|
+
if (result.audit !== "browser") {
|
|
544
|
+
throw new Error(`Expected a 'browser' AuditResult, got '${result.audit}'`);
|
|
545
|
+
}
|
|
546
|
+
const d = result.details;
|
|
547
|
+
return {
|
|
548
|
+
desktopOk: d?.desktopOk === true,
|
|
549
|
+
mobileOk: d?.mobileOk === true,
|
|
550
|
+
linksOk: d?.linksOk === true,
|
|
551
|
+
brokenLinks: typeof d?.brokenLinks === "number" ? d.brokenLinks : 0,
|
|
552
|
+
checkedAt: typeof d?.checkedAt === "string" ? d.checkedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
var init_browser_airtable = __esm({
|
|
556
|
+
"src/audits/browser-airtable.ts"() {
|
|
557
|
+
"use strict";
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
477
561
|
// src/audits/write-audits-to-airtable.ts
|
|
478
562
|
var write_audits_to_airtable_exports = {};
|
|
479
563
|
__export(write_audits_to_airtable_exports, {
|
|
@@ -484,22 +568,14 @@ __export(write_audits_to_airtable_exports, {
|
|
|
484
568
|
async function writeAuditsToAirtable(args) {
|
|
485
569
|
const { base, websites, slug, results } = args;
|
|
486
570
|
const lhResult = results.find((r) => r.audit === "lighthouse");
|
|
487
|
-
if (!lhResult) {
|
|
488
|
-
throw Object.assign(
|
|
489
|
-
new Error(
|
|
490
|
-
"--write-airtable requires a lighthouse result; did you pass --only without lighthouse?"
|
|
491
|
-
),
|
|
492
|
-
{ exitCode: 2 }
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
571
|
const target = websites.find((w) => siteSlug(w.name) === slug);
|
|
496
572
|
if (!target) {
|
|
497
573
|
throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
|
|
498
574
|
}
|
|
499
575
|
const writes = [];
|
|
500
576
|
const audits = {};
|
|
501
|
-
const lhHasScores = hasRealScores(lhResult);
|
|
502
|
-
if (lhHasScores) {
|
|
577
|
+
const lhHasScores = lhResult ? hasRealScores(lhResult) : false;
|
|
578
|
+
if (lhResult && lhHasScores) {
|
|
503
579
|
const scores = lighthouseScoresFromResult(lhResult);
|
|
504
580
|
audits.scores = scores;
|
|
505
581
|
writes.push({ audit: "lighthouse", counts: scores });
|
|
@@ -522,10 +598,22 @@ async function writeAuditsToAirtable(args) {
|
|
|
522
598
|
audits.security = counts;
|
|
523
599
|
writes.push({ audit: "security", counts });
|
|
524
600
|
}
|
|
601
|
+
const dom = results.find((r) => r.audit === "domain");
|
|
602
|
+
if (dom && hasDomainResult(dom)) {
|
|
603
|
+
const result = domainResultFromAudit(dom);
|
|
604
|
+
audits.domain = result;
|
|
605
|
+
writes.push({ audit: "domain", counts: result });
|
|
606
|
+
}
|
|
607
|
+
const browser = results.find((r) => r.audit === "browser");
|
|
608
|
+
if (browser && hasBrowserResult(browser)) {
|
|
609
|
+
const fields = browserFieldsFromAudit(browser);
|
|
610
|
+
audits.browser = fields;
|
|
611
|
+
writes.push({ audit: "browser", counts: fields });
|
|
612
|
+
}
|
|
525
613
|
if (Object.keys(audits).length > 0) {
|
|
526
614
|
await updateAuditFields(base, target.id, audits);
|
|
527
615
|
}
|
|
528
|
-
if (!lhHasScores) {
|
|
616
|
+
if (lhResult && !lhHasScores) {
|
|
529
617
|
const persisted = writes.map((w) => w.audit);
|
|
530
618
|
throw Object.assign(
|
|
531
619
|
new Error(
|
|
@@ -576,6 +664,8 @@ var init_write_audits_to_airtable = __esm({
|
|
|
576
664
|
init_a11y_airtable();
|
|
577
665
|
init_deps_airtable();
|
|
578
666
|
init_security_airtable();
|
|
667
|
+
init_domain_airtable();
|
|
668
|
+
init_browser_airtable();
|
|
579
669
|
}
|
|
580
670
|
});
|
|
581
671
|
|
|
@@ -661,9 +751,32 @@ function mapRow2(rec) {
|
|
|
661
751
|
deliveryStatus: f["Delivery status"] ?? "pending",
|
|
662
752
|
renderedHtmlAttachment: html,
|
|
663
753
|
resendMessageId: f["Resend message ID"] ?? null,
|
|
664
|
-
checklist: Object.fromEntries(ALL_CHECKLIST_FIELDS.map((name) => [name, Boolean(f[name])]))
|
|
754
|
+
checklist: Object.fromEntries(ALL_CHECKLIST_FIELDS.map((name) => [name, Boolean(f[name])])),
|
|
755
|
+
autoEvidence: parseAutoEvidence(f["Checklist auto-evidence"])
|
|
665
756
|
};
|
|
666
757
|
}
|
|
758
|
+
function parseAutoEvidence(raw) {
|
|
759
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
760
|
+
let parsed;
|
|
761
|
+
try {
|
|
762
|
+
parsed = JSON.parse(raw);
|
|
763
|
+
} catch {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
767
|
+
const out = {};
|
|
768
|
+
for (const [field, v] of Object.entries(parsed)) {
|
|
769
|
+
if (!v || typeof v !== "object") continue;
|
|
770
|
+
const o = v;
|
|
771
|
+
if (o.result !== "pass" && o.result !== "fail" && o.result !== "unknown") continue;
|
|
772
|
+
out[field] = {
|
|
773
|
+
result: o.result,
|
|
774
|
+
checkedAt: typeof o.checkedAt === "string" ? o.checkedAt : null,
|
|
775
|
+
note: typeof o.note === "string" ? o.note : ""
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
779
|
+
}
|
|
667
780
|
function lighthouseFromFields(f) {
|
|
668
781
|
const p = f["Lighthouse \u2014 Performance"];
|
|
669
782
|
const a = f["Lighthouse \u2014 Accessibility"];
|
|
@@ -700,6 +813,10 @@ async function createDraft(base, input) {
|
|
|
700
813
|
if (input.searchPosition !== void 0) fields["Search position"] = input.searchPosition;
|
|
701
814
|
if (input.period !== void 0) fields["Period"] = input.period;
|
|
702
815
|
if (input.subjectOverride !== void 0) fields["Subject override"] = input.subjectOverride;
|
|
816
|
+
for (const field of input.checklistTicks ?? []) fields[field] = true;
|
|
817
|
+
if (input.autoEvidence && Object.keys(input.autoEvidence).length > 0) {
|
|
818
|
+
fields["Checklist auto-evidence"] = JSON.stringify(input.autoEvidence);
|
|
819
|
+
}
|
|
703
820
|
const created = await base(REPORTS_TABLE).create([{ fields }]);
|
|
704
821
|
const rec = created[0];
|
|
705
822
|
if (!rec) throw new Error("Airtable create returned no records");
|
|
@@ -1470,7 +1587,7 @@ function gitHubSignalsStale(swept, now) {
|
|
|
1470
1587
|
if (swept === null) return true;
|
|
1471
1588
|
const ageMs = now.getTime() - Date.parse(swept);
|
|
1472
1589
|
if (!Number.isFinite(ageMs)) return true;
|
|
1473
|
-
return ageMs > GITHUB_SIGNALS_STALE_DAYS *
|
|
1590
|
+
return ageMs > GITHUB_SIGNALS_STALE_DAYS * MS_PER_DAY4;
|
|
1474
1591
|
}
|
|
1475
1592
|
function collectVulnAlerts(sites, baseUrl) {
|
|
1476
1593
|
const items = [];
|
|
@@ -1564,13 +1681,13 @@ function collectCiAlerts(sites, baseUrl, now = /* @__PURE__ */ new Date()) {
|
|
|
1564
1681
|
}
|
|
1565
1682
|
return items;
|
|
1566
1683
|
}
|
|
1567
|
-
var GITHUB_SIGNALS_STALE_DAYS,
|
|
1684
|
+
var GITHUB_SIGNALS_STALE_DAYS, MS_PER_DAY4, LIGHTHOUSE_FLOOR, LIGHTHOUSE_CATEGORIES2;
|
|
1568
1685
|
var init_digest_collectors = __esm({
|
|
1569
1686
|
"src/alerts/digest-collectors.ts"() {
|
|
1570
1687
|
"use strict";
|
|
1571
1688
|
init_websites();
|
|
1572
1689
|
GITHUB_SIGNALS_STALE_DAYS = 3;
|
|
1573
|
-
|
|
1690
|
+
MS_PER_DAY4 = 24 * 60 * 60 * 1e3;
|
|
1574
1691
|
LIGHTHOUSE_FLOOR = 75;
|
|
1575
1692
|
LIGHTHOUSE_CATEGORIES2 = [
|
|
1576
1693
|
{ field: "pScore", slug: "performance", label: "Performance" },
|
|
@@ -2015,35 +2132,33 @@ async function sendOne(client, base, site, report) {
|
|
|
2015
2132
|
});
|
|
2016
2133
|
const reportDate = report.completedOn ? new Date(report.completedOn) : /* @__PURE__ */ new Date();
|
|
2017
2134
|
const subject = report.subjectOverride ?? `${site.name} \u2014 ${monthYear(reportDate)} ${report.reportType} Report`;
|
|
2135
|
+
const attachments = [
|
|
2136
|
+
toInlineAttachment({
|
|
2137
|
+
bytes: header.bytes,
|
|
2138
|
+
filename: `${cidName}.jpg`,
|
|
2139
|
+
contentType: header.contentType,
|
|
2140
|
+
cid: cidName
|
|
2141
|
+
})
|
|
2142
|
+
];
|
|
2143
|
+
for (const img of [bundled.check, bundled.blurred]) {
|
|
2144
|
+
if (html.includes(`cid:${img.cid}`)) {
|
|
2145
|
+
attachments.push(
|
|
2146
|
+
toInlineAttachment({
|
|
2147
|
+
bytes: img.bytes,
|
|
2148
|
+
filename: img.filename,
|
|
2149
|
+
contentType: img.contentType,
|
|
2150
|
+
cid: img.cid
|
|
2151
|
+
})
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2018
2155
|
const payload = {
|
|
2019
2156
|
from: FROM_ADDRESS2,
|
|
2020
2157
|
to,
|
|
2021
2158
|
replyTo: REPLY_TO,
|
|
2022
2159
|
subject,
|
|
2023
2160
|
html,
|
|
2024
|
-
attachments
|
|
2025
|
-
toInlineAttachment({
|
|
2026
|
-
bytes: header.bytes,
|
|
2027
|
-
filename: `${cidName}.jpg`,
|
|
2028
|
-
contentType: header.contentType,
|
|
2029
|
-
cid: cidName
|
|
2030
|
-
}),
|
|
2031
|
-
// Bundled images referenced via cid:rd-check-png / cid:rd-blurred-tests-jpg
|
|
2032
|
-
// in the template. Attached inline so the email is self-contained — no
|
|
2033
|
-
// external CDN dependency, no image-blocked broken icons in webmail.
|
|
2034
|
-
toInlineAttachment({
|
|
2035
|
-
bytes: bundled.check.bytes,
|
|
2036
|
-
filename: bundled.check.filename,
|
|
2037
|
-
contentType: bundled.check.contentType,
|
|
2038
|
-
cid: bundled.check.cid
|
|
2039
|
-
}),
|
|
2040
|
-
toInlineAttachment({
|
|
2041
|
-
bytes: bundled.blurred.bytes,
|
|
2042
|
-
filename: bundled.blurred.filename,
|
|
2043
|
-
contentType: bundled.blurred.contentType,
|
|
2044
|
-
cid: bundled.blurred.cid
|
|
2045
|
-
})
|
|
2046
|
-
],
|
|
2161
|
+
attachments,
|
|
2047
2162
|
// Stable across retries of the same row — if Airtable stamping fails after a
|
|
2048
2163
|
// successful Resend, the next --send-ready replays with the same key and
|
|
2049
2164
|
// Resend returns the original message id rather than sending a duplicate.
|
|
@@ -3073,13 +3188,348 @@ async function a11yAudit(ctx) {
|
|
|
3073
3188
|
}
|
|
3074
3189
|
}
|
|
3075
3190
|
|
|
3191
|
+
// src/audits/domain.ts
|
|
3192
|
+
import { promises as dnsPromises } from "dns";
|
|
3193
|
+
import tls from "tls";
|
|
3194
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
3195
|
+
async function checkDomain(url, deps) {
|
|
3196
|
+
let host;
|
|
3197
|
+
try {
|
|
3198
|
+
host = new URL(url).hostname;
|
|
3199
|
+
} catch {
|
|
3200
|
+
return { resolved: false, certDaysRemaining: null };
|
|
3201
|
+
}
|
|
3202
|
+
try {
|
|
3203
|
+
await deps.lookup(host);
|
|
3204
|
+
} catch {
|
|
3205
|
+
return { resolved: false, certDaysRemaining: null };
|
|
3206
|
+
}
|
|
3207
|
+
let validTo;
|
|
3208
|
+
try {
|
|
3209
|
+
validTo = await deps.certValidTo(host);
|
|
3210
|
+
} catch {
|
|
3211
|
+
validTo = null;
|
|
3212
|
+
}
|
|
3213
|
+
if (!validTo || Number.isNaN(validTo.getTime()))
|
|
3214
|
+
return { resolved: true, certDaysRemaining: null };
|
|
3215
|
+
return {
|
|
3216
|
+
resolved: true,
|
|
3217
|
+
certDaysRemaining: Math.floor((validTo.getTime() - deps.now.getTime()) / MS_PER_DAY)
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
function defaultDomainDeps(now) {
|
|
3221
|
+
return {
|
|
3222
|
+
lookup: async (host) => {
|
|
3223
|
+
await dnsPromises.lookup(host);
|
|
3224
|
+
},
|
|
3225
|
+
certValidTo: (host) => new Promise((resolvePromise) => {
|
|
3226
|
+
const socket = tls.connect(
|
|
3227
|
+
{ host, port: 443, servername: host, timeout: 1e4, rejectUnauthorized: true },
|
|
3228
|
+
() => {
|
|
3229
|
+
const cert = socket.authorized ? socket.getPeerCertificate() : null;
|
|
3230
|
+
socket.end();
|
|
3231
|
+
const validTo = cert && cert.valid_to ? new Date(cert.valid_to) : null;
|
|
3232
|
+
resolvePromise(validTo);
|
|
3233
|
+
}
|
|
3234
|
+
);
|
|
3235
|
+
socket.on("error", () => resolvePromise(null));
|
|
3236
|
+
socket.on("timeout", () => {
|
|
3237
|
+
socket.destroy();
|
|
3238
|
+
resolvePromise(null);
|
|
3239
|
+
});
|
|
3240
|
+
}),
|
|
3241
|
+
now
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
async function domainAudit(ctx) {
|
|
3245
|
+
const { site } = ctx;
|
|
3246
|
+
const label = siteLabel(site);
|
|
3247
|
+
if (!site.deployedUrl) {
|
|
3248
|
+
return { audit: "domain", site: label, status: "skip", summary: "no deployed URL" };
|
|
3249
|
+
}
|
|
3250
|
+
const now = ctx.now ?? /* @__PURE__ */ new Date();
|
|
3251
|
+
const deps = ctx.domainDeps ?? defaultDomainDeps(now);
|
|
3252
|
+
const check = await checkDomain(site.deployedUrl, deps);
|
|
3253
|
+
const checkedAt = now.toISOString();
|
|
3254
|
+
const status = check.resolved && check.certDaysRemaining !== null && check.certDaysRemaining > 14 ? "pass" : "warn";
|
|
3255
|
+
const summary = !check.resolved ? "did not resolve" : check.certDaysRemaining === null ? "resolved, no usable TLS cert" : `resolved, cert ${check.certDaysRemaining}d remaining`;
|
|
3256
|
+
return {
|
|
3257
|
+
audit: "domain",
|
|
3258
|
+
site: label,
|
|
3259
|
+
status,
|
|
3260
|
+
summary,
|
|
3261
|
+
details: { resolved: check.resolved, certDaysRemaining: check.certDaysRemaining, checkedAt }
|
|
3262
|
+
};
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
// src/audits/route-discovery.ts
|
|
3266
|
+
var DEFAULT_CAP = 15;
|
|
3267
|
+
function parseSitemapUrls(xml) {
|
|
3268
|
+
const out = [];
|
|
3269
|
+
const re = /<loc>\s*([^<\s]+)\s*<\/loc>/gi;
|
|
3270
|
+
let m;
|
|
3271
|
+
while ((m = re.exec(xml)) !== null) {
|
|
3272
|
+
const url = m[1];
|
|
3273
|
+
if (url) out.push(url.trim());
|
|
3274
|
+
}
|
|
3275
|
+
return out;
|
|
3276
|
+
}
|
|
3277
|
+
function parseHtmlLinks(html, baseUrl) {
|
|
3278
|
+
const out = /* @__PURE__ */ new Set();
|
|
3279
|
+
const re = /<a\b[^>]*\bhref\s*=\s*["']([^"']+)["']/gi;
|
|
3280
|
+
let m;
|
|
3281
|
+
while ((m = re.exec(html)) !== null) {
|
|
3282
|
+
const href = m[1];
|
|
3283
|
+
if (!href || href.startsWith("#") || /^(mailto:|tel:|javascript:)/i.test(href)) continue;
|
|
3284
|
+
try {
|
|
3285
|
+
const u = new URL(href, baseUrl);
|
|
3286
|
+
if (u.origin !== new URL(baseUrl).origin) continue;
|
|
3287
|
+
out.add(u.pathname);
|
|
3288
|
+
} catch {
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
return [...out];
|
|
3292
|
+
}
|
|
3293
|
+
function family(pathname) {
|
|
3294
|
+
return pathname.split("/").filter(Boolean)[0] ?? "";
|
|
3295
|
+
}
|
|
3296
|
+
function sampleRoutePaths(urlsOrPaths, cap = DEFAULT_CAP) {
|
|
3297
|
+
const seen = /* @__PURE__ */ new Set(["/"]);
|
|
3298
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3299
|
+
for (const raw of urlsOrPaths) {
|
|
3300
|
+
let pathname;
|
|
3301
|
+
try {
|
|
3302
|
+
pathname = raw.startsWith("/") ? new URL(raw, "https://x.invalid").pathname : new URL(raw).pathname;
|
|
3303
|
+
} catch {
|
|
3304
|
+
continue;
|
|
3305
|
+
}
|
|
3306
|
+
if (pathname === "/") continue;
|
|
3307
|
+
if (seen.has(pathname)) continue;
|
|
3308
|
+
seen.add(pathname);
|
|
3309
|
+
const fam = family(pathname);
|
|
3310
|
+
const arr = buckets.get(fam) ?? [];
|
|
3311
|
+
arr.push(pathname);
|
|
3312
|
+
buckets.set(fam, arr);
|
|
3313
|
+
}
|
|
3314
|
+
const result = ["/"];
|
|
3315
|
+
const families = [...buckets.values()];
|
|
3316
|
+
let guard = 0;
|
|
3317
|
+
while (result.length < cap && families.some((f) => f.length > 0) && guard++ < 1e4) {
|
|
3318
|
+
for (const fam of families) {
|
|
3319
|
+
if (result.length >= cap) break;
|
|
3320
|
+
const next = fam.shift();
|
|
3321
|
+
if (next) result.push(next);
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
return result;
|
|
3325
|
+
}
|
|
3326
|
+
function familyCountsOf(paths) {
|
|
3327
|
+
const counts = {};
|
|
3328
|
+
for (const p of paths) {
|
|
3329
|
+
const key = p === "/" ? "/" : `/${family(p)}`;
|
|
3330
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
3331
|
+
}
|
|
3332
|
+
return counts;
|
|
3333
|
+
}
|
|
3334
|
+
async function discoverRoutes(deployedUrl, deps, cap = DEFAULT_CAP) {
|
|
3335
|
+
const origin = new URL(deployedUrl).origin;
|
|
3336
|
+
const abs = (paths) => paths.map((p) => new URL(p, origin).href);
|
|
3337
|
+
const sitemapXml = await deps.fetchText(new URL("/sitemap.xml", origin).href);
|
|
3338
|
+
if (sitemapXml) {
|
|
3339
|
+
const urls = parseSitemapUrls(sitemapXml);
|
|
3340
|
+
if (urls.length > 0) {
|
|
3341
|
+
const paths = sampleRoutePaths(urls, cap);
|
|
3342
|
+
return { routes: abs(paths), source: "sitemap", familyCounts: familyCountsOf(paths) };
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
const homeHtml = await deps.fetchText(origin);
|
|
3346
|
+
if (homeHtml) {
|
|
3347
|
+
const links = parseHtmlLinks(homeHtml, origin);
|
|
3348
|
+
if (links.length > 0) {
|
|
3349
|
+
const paths = sampleRoutePaths(links, cap);
|
|
3350
|
+
return { routes: abs(paths), source: "homepage-links", familyCounts: familyCountsOf(paths) };
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
return { routes: [new URL("/", origin).href], source: "root-only", familyCounts: { "/": 1 } };
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
// src/audits/browser.ts
|
|
3357
|
+
function isBroken(status) {
|
|
3358
|
+
return status === null || status >= 400;
|
|
3359
|
+
}
|
|
3360
|
+
function summarizeBrowser(routes, links, familyCounts) {
|
|
3361
|
+
const desktopChecks = routes.flatMap((r) => r.desktop);
|
|
3362
|
+
const mobileChecks = routes.flatMap((r) => r.mobile);
|
|
3363
|
+
const desktopOk = routes.length > 0 && routes.every((r) => r.desktop.length > 0 && r.desktop.every((d) => d.ok));
|
|
3364
|
+
const mobileOk = routes.length > 0 && routes.every((r) => r.mobile.length > 0 && r.mobile.every((m) => m.ok));
|
|
3365
|
+
const brokenLinks = links.filter((l) => isBroken(l.status)).length;
|
|
3366
|
+
const linksOk = links.length > 0 && brokenLinks === 0;
|
|
3367
|
+
const engines = [...new Set(desktopChecks.map((d) => d.engine))];
|
|
3368
|
+
const devices2 = [...new Set(mobileChecks.map((m) => m.device))];
|
|
3369
|
+
const families = Object.entries(familyCounts).map(([f, n]) => f === "/" ? "/" : `${f} \xD7${n}`).join(", ");
|
|
3370
|
+
const note = `${routes.length} routes (${families}); desktop ${engines.join("/") || "\u2014"}; mobile ${devices2.join("/") || "\u2014"}; ${links.length} links, ${brokenLinks} broken`;
|
|
3371
|
+
return { desktopOk, mobileOk, linksOk, brokenLinks, routesChecked: routes.length, note };
|
|
3372
|
+
}
|
|
3373
|
+
async function browserAudit(ctx) {
|
|
3374
|
+
const { site } = ctx;
|
|
3375
|
+
const label = siteLabel(site);
|
|
3376
|
+
if (!site.deployedUrl) {
|
|
3377
|
+
return { audit: "browser", site: label, status: "skip", summary: "no deployed URL" };
|
|
3378
|
+
}
|
|
3379
|
+
const now = ctx.now ?? /* @__PURE__ */ new Date();
|
|
3380
|
+
const discoverDeps = ctx.discoverDeps ?? defaultDiscoverDeps();
|
|
3381
|
+
const runner = ctx.browserRunner ?? await defaultBrowserRunner();
|
|
3382
|
+
try {
|
|
3383
|
+
const discovered = await discoverRoutes(site.deployedUrl, discoverDeps);
|
|
3384
|
+
const routeResults = await runner.probe(discovered.routes);
|
|
3385
|
+
const internalLinks = [...new Set(routeResults.flatMap((r) => r.links))];
|
|
3386
|
+
const linkResults = await runner.checkLinks(internalLinks);
|
|
3387
|
+
const summary = summarizeBrowser(
|
|
3388
|
+
routeResults,
|
|
3389
|
+
linkResults,
|
|
3390
|
+
discovered.familyCounts ?? familyCountsOf(discovered.routes)
|
|
3391
|
+
);
|
|
3392
|
+
const status = summary.desktopOk && summary.mobileOk && summary.linksOk ? "pass" : "warn";
|
|
3393
|
+
return {
|
|
3394
|
+
audit: "browser",
|
|
3395
|
+
site: label,
|
|
3396
|
+
status,
|
|
3397
|
+
summary: summary.note,
|
|
3398
|
+
details: { ...summary, checkedAt: now.toISOString() }
|
|
3399
|
+
};
|
|
3400
|
+
} finally {
|
|
3401
|
+
await runner.close?.();
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
function defaultDiscoverDeps() {
|
|
3405
|
+
return {
|
|
3406
|
+
fetchText: async (url) => {
|
|
3407
|
+
try {
|
|
3408
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
3409
|
+
if (!res.ok) return null;
|
|
3410
|
+
return await res.text();
|
|
3411
|
+
} catch {
|
|
3412
|
+
return null;
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
var DESKTOP_VIEWPORT = { width: 1366, height: 900 };
|
|
3418
|
+
var PAGE_TIMEOUT_MS = 3e4;
|
|
3419
|
+
async function defaultBrowserRunner() {
|
|
3420
|
+
const { chromium, firefox, webkit, devices: devices2 } = await import("@playwright/test");
|
|
3421
|
+
const desktopEngines = [
|
|
3422
|
+
{ engine: "chromium", type: chromium },
|
|
3423
|
+
{ engine: "firefox", type: firefox },
|
|
3424
|
+
{ engine: "webkit", type: webkit }
|
|
3425
|
+
];
|
|
3426
|
+
const mobileTargets = [
|
|
3427
|
+
{ device: "Pixel 7", descriptor: devices2["Pixel 7"] },
|
|
3428
|
+
{ device: "iPhone 14", descriptor: devices2["iPhone 14"] }
|
|
3429
|
+
];
|
|
3430
|
+
return {
|
|
3431
|
+
async probe(urls) {
|
|
3432
|
+
const results = [];
|
|
3433
|
+
const browsers = await Promise.all(desktopEngines.map((e) => e.type.launch()));
|
|
3434
|
+
const mobileBrowsers = await Promise.all(mobileTargets.map(() => chromium.launch()));
|
|
3435
|
+
try {
|
|
3436
|
+
for (const url of urls) {
|
|
3437
|
+
const desktop = [];
|
|
3438
|
+
const linkSet = /* @__PURE__ */ new Set();
|
|
3439
|
+
for (let i = 0; i < desktopEngines.length; i++) {
|
|
3440
|
+
const engine = desktopEngines[i].engine;
|
|
3441
|
+
const browser = browsers[i];
|
|
3442
|
+
const ctx = await browser.newContext({ viewport: DESKTOP_VIEWPORT });
|
|
3443
|
+
const page = await ctx.newPage();
|
|
3444
|
+
const errors = [];
|
|
3445
|
+
page.on("pageerror", (e) => errors.push(String(e)));
|
|
3446
|
+
let ok = false;
|
|
3447
|
+
try {
|
|
3448
|
+
const resp = await page.goto(url, {
|
|
3449
|
+
waitUntil: "domcontentloaded",
|
|
3450
|
+
timeout: PAGE_TIMEOUT_MS
|
|
3451
|
+
});
|
|
3452
|
+
const hasMain = await page.locator("main, [role=main]").first().isVisible().catch(() => false);
|
|
3453
|
+
ok = !!resp && resp.ok() && errors.length === 0 && hasMain;
|
|
3454
|
+
if (engine === "chromium") {
|
|
3455
|
+
const hrefs = await page.evaluate("Array.from(document.querySelectorAll('a[href]')).map((a) => a.href)").catch(() => []);
|
|
3456
|
+
const origin = new URL(url).origin;
|
|
3457
|
+
for (const h of hrefs) {
|
|
3458
|
+
try {
|
|
3459
|
+
if (new URL(h).origin === origin) linkSet.add(new URL(h).href);
|
|
3460
|
+
} catch {
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
} catch {
|
|
3465
|
+
ok = false;
|
|
3466
|
+
} finally {
|
|
3467
|
+
await ctx.close().catch(() => {
|
|
3468
|
+
});
|
|
3469
|
+
}
|
|
3470
|
+
desktop.push({ engine, ok });
|
|
3471
|
+
}
|
|
3472
|
+
const mobile = [];
|
|
3473
|
+
for (let i = 0; i < mobileTargets.length; i++) {
|
|
3474
|
+
const { device, descriptor } = mobileTargets[i];
|
|
3475
|
+
const browser = mobileBrowsers[i];
|
|
3476
|
+
const ctx = await browser.newContext({ ...descriptor });
|
|
3477
|
+
const page = await ctx.newPage();
|
|
3478
|
+
const errors = [];
|
|
3479
|
+
page.on("pageerror", (e) => errors.push(String(e)));
|
|
3480
|
+
let ok = false;
|
|
3481
|
+
try {
|
|
3482
|
+
const resp = await page.goto(url, {
|
|
3483
|
+
waitUntil: "domcontentloaded",
|
|
3484
|
+
timeout: PAGE_TIMEOUT_MS
|
|
3485
|
+
});
|
|
3486
|
+
const overflow = await page.evaluate("document.documentElement.scrollWidth > window.innerWidth + 2").catch(() => true);
|
|
3487
|
+
ok = !!resp && resp.ok() && errors.length === 0 && !overflow;
|
|
3488
|
+
} catch {
|
|
3489
|
+
ok = false;
|
|
3490
|
+
} finally {
|
|
3491
|
+
await ctx.close().catch(() => {
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
mobile.push({ device, ok });
|
|
3495
|
+
}
|
|
3496
|
+
results.push({ url, desktop, mobile, links: [...linkSet] });
|
|
3497
|
+
}
|
|
3498
|
+
} finally {
|
|
3499
|
+
await Promise.all([...browsers, ...mobileBrowsers].map((b) => b.close().catch(() => {
|
|
3500
|
+
})));
|
|
3501
|
+
}
|
|
3502
|
+
return results;
|
|
3503
|
+
},
|
|
3504
|
+
async checkLinks(urls) {
|
|
3505
|
+
const out = [];
|
|
3506
|
+
for (const url of urls) {
|
|
3507
|
+
let status;
|
|
3508
|
+
try {
|
|
3509
|
+
let res = await fetch(url, { method: "HEAD", redirect: "follow" });
|
|
3510
|
+
if (res.status === 405 || res.status === 501) {
|
|
3511
|
+
res = await fetch(url, { method: "GET", redirect: "follow" });
|
|
3512
|
+
}
|
|
3513
|
+
status = res.status;
|
|
3514
|
+
} catch {
|
|
3515
|
+
status = null;
|
|
3516
|
+
}
|
|
3517
|
+
out.push({ url, status });
|
|
3518
|
+
}
|
|
3519
|
+
return out;
|
|
3520
|
+
}
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3076
3524
|
// src/audits/index.ts
|
|
3077
3525
|
var REGISTRY = {
|
|
3078
3526
|
deps: depsAudit,
|
|
3079
3527
|
lint: lintAudit,
|
|
3080
3528
|
security: securityAudit,
|
|
3081
3529
|
lighthouse: lighthouseAudit,
|
|
3082
|
-
a11y: a11yAudit
|
|
3530
|
+
a11y: a11yAudit,
|
|
3531
|
+
domain: domainAudit,
|
|
3532
|
+
browser: browserAudit
|
|
3083
3533
|
};
|
|
3084
3534
|
var ALL_AUDIT_NAMES = Object.keys(REGISTRY);
|
|
3085
3535
|
var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
|
|
@@ -3556,8 +4006,13 @@ function deployedUrlNotice(which, url, cwd) {
|
|
|
3556
4006
|
if (others.length === 0) return null;
|
|
3557
4007
|
return `note: --url only affects lighthouse; ${others.join(", ")} ran against the local checkout at ${cwd}`;
|
|
3558
4008
|
}
|
|
4009
|
+
var CHECKOUT_FREE_AUDITS = /* @__PURE__ */ new Set([
|
|
4010
|
+
"lighthouse",
|
|
4011
|
+
"domain",
|
|
4012
|
+
"browser"
|
|
4013
|
+
]);
|
|
3559
4014
|
function auditNeedsCheckout(site, which) {
|
|
3560
|
-
const deployedCapable = site.deployedUrl !== void 0 && which.every((n) => n
|
|
4015
|
+
const deployedCapable = site.deployedUrl !== void 0 && which.every((n) => CHECKOUT_FREE_AUDITS.has(n));
|
|
3561
4016
|
return !deployedCapable;
|
|
3562
4017
|
}
|
|
3563
4018
|
function applyDeployedUrl(sites, url) {
|
|
@@ -5811,6 +6266,120 @@ async function queueDraft(base, report) {
|
|
|
5811
6266
|
return { queued: true, supersededIds };
|
|
5812
6267
|
}
|
|
5813
6268
|
|
|
6269
|
+
// src/reports/auto-tick.ts
|
|
6270
|
+
init_checklist();
|
|
6271
|
+
init_url();
|
|
6272
|
+
var STALE_DAYS = 3;
|
|
6273
|
+
var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
|
|
6274
|
+
function isFresh(checkedAt, now) {
|
|
6275
|
+
if (!checkedAt) return false;
|
|
6276
|
+
const t = new Date(checkedAt).getTime();
|
|
6277
|
+
if (Number.isNaN(t)) return false;
|
|
6278
|
+
return now.getTime() - t <= STALE_DAYS * MS_PER_DAY2;
|
|
6279
|
+
}
|
|
6280
|
+
var CERT_MIN_DAYS = 14;
|
|
6281
|
+
function autoTickChecklist(site, reportType, now, signals) {
|
|
6282
|
+
const out = /* @__PURE__ */ new Map();
|
|
6283
|
+
const fields = new Set(checklistFor(reportType).map((i) => i.field));
|
|
6284
|
+
if (fields.has("Maint: Google Indexed")) {
|
|
6285
|
+
const g = googleEvidence(now, signals.search);
|
|
6286
|
+
if (g) out.set("Maint: Google Indexed", g);
|
|
6287
|
+
}
|
|
6288
|
+
if (fields.has("Maint: Security Updates")) {
|
|
6289
|
+
const s = securityEvidence(site, now);
|
|
6290
|
+
if (s) out.set("Maint: Security Updates", s);
|
|
6291
|
+
}
|
|
6292
|
+
if (fields.has("Maint: Domain, DNS & SSL")) {
|
|
6293
|
+
const d = domainEvidence(site, now);
|
|
6294
|
+
if (d) out.set("Maint: Domain, DNS & SSL", d);
|
|
6295
|
+
}
|
|
6296
|
+
if (fields.has("Test: Desktop Browsers")) {
|
|
6297
|
+
const e = browserEvidence(
|
|
6298
|
+
site.crossbrowserOk,
|
|
6299
|
+
site,
|
|
6300
|
+
now,
|
|
6301
|
+
"Desktop renders cleanly",
|
|
6302
|
+
"render errors"
|
|
6303
|
+
);
|
|
6304
|
+
if (e) out.set("Test: Desktop Browsers", e);
|
|
6305
|
+
}
|
|
6306
|
+
if (fields.has("Test: Mobile Browsers")) {
|
|
6307
|
+
const e = browserEvidence(
|
|
6308
|
+
site.mobileOk,
|
|
6309
|
+
site,
|
|
6310
|
+
now,
|
|
6311
|
+
"Mobile renders cleanly",
|
|
6312
|
+
"overflow/errors"
|
|
6313
|
+
);
|
|
6314
|
+
if (e) out.set("Test: Mobile Browsers", e);
|
|
6315
|
+
}
|
|
6316
|
+
if (fields.has("Test: Links & Navigation")) {
|
|
6317
|
+
const broken = site.brokenLinks;
|
|
6318
|
+
const failNote = broken && broken > 0 ? `${broken} broken link(s)` : "broken links / nav";
|
|
6319
|
+
const e = browserEvidence(site.linksOk, site, now, "All internal links resolve", failNote);
|
|
6320
|
+
if (e) out.set("Test: Links & Navigation", e);
|
|
6321
|
+
}
|
|
6322
|
+
return out;
|
|
6323
|
+
}
|
|
6324
|
+
function browserEvidence(ok, site, now, passNote, failNote) {
|
|
6325
|
+
if (ok === null || !site.browserCheckedAt) return null;
|
|
6326
|
+
const at = site.browserCheckedAt;
|
|
6327
|
+
if (!isFresh(at, now)) {
|
|
6328
|
+
return { result: "unknown", checkedAt: at, note: "Browser check is stale (>3d)" };
|
|
6329
|
+
}
|
|
6330
|
+
return ok ? { result: "pass", checkedAt: at, note: passNote } : { result: "fail", checkedAt: at, note: failNote };
|
|
6331
|
+
}
|
|
6332
|
+
function securityEvidence(site, now) {
|
|
6333
|
+
const crit = site.securityVulnsCritical;
|
|
6334
|
+
const high = site.securityVulnsHigh;
|
|
6335
|
+
if (crit === null || high === null || !site.lastSecurityAuditAt) return null;
|
|
6336
|
+
const at = site.lastSecurityAuditAt;
|
|
6337
|
+
if (!isFresh(at, now)) {
|
|
6338
|
+
return { result: "unknown", checkedAt: at, note: "Security audit is stale (>3d)" };
|
|
6339
|
+
}
|
|
6340
|
+
if (crit === 0 && high === 0) {
|
|
6341
|
+
return { result: "pass", checkedAt: at, note: "No known critical/high vulnerabilities" };
|
|
6342
|
+
}
|
|
6343
|
+
return { result: "fail", checkedAt: at, note: `${crit} critical / ${high} high vuln(s)` };
|
|
6344
|
+
}
|
|
6345
|
+
function googleEvidence(now, search) {
|
|
6346
|
+
const at = now.toISOString();
|
|
6347
|
+
if (search.softFailed) {
|
|
6348
|
+
return { result: "unknown", checkedAt: at, note: "Search Console unavailable this run" };
|
|
6349
|
+
}
|
|
6350
|
+
if (search.value === null) return null;
|
|
6351
|
+
if (search.value.foundOnPage1) {
|
|
6352
|
+
const pos2 = search.value.position;
|
|
6353
|
+
return {
|
|
6354
|
+
result: "pass",
|
|
6355
|
+
checkedAt: at,
|
|
6356
|
+
note: `Page 1 on Google${pos2 !== null ? ` (#${pos2})` : ""}`
|
|
6357
|
+
};
|
|
6358
|
+
}
|
|
6359
|
+
const pos = search.value.position;
|
|
6360
|
+
return {
|
|
6361
|
+
result: "fail",
|
|
6362
|
+
checkedAt: at,
|
|
6363
|
+
note: `Not on page 1${pos !== null ? ` (avg #${pos})` : ""}`
|
|
6364
|
+
};
|
|
6365
|
+
}
|
|
6366
|
+
function domainEvidence(site, now) {
|
|
6367
|
+
if (!site.url || isNetlifyAppUrl(site.url)) return null;
|
|
6368
|
+
if (!site.domainCheckedAt) return null;
|
|
6369
|
+
const at = site.domainCheckedAt;
|
|
6370
|
+
if (!isFresh(site.domainCheckedAt, now)) {
|
|
6371
|
+
return { result: "unknown", checkedAt: at, note: "Domain check is stale (>3d)" };
|
|
6372
|
+
}
|
|
6373
|
+
const days = site.certDaysRemaining;
|
|
6374
|
+
if (days === null) {
|
|
6375
|
+
return { result: "fail", checkedAt: at, note: "Did not resolve, or no valid TLS cert" };
|
|
6376
|
+
}
|
|
6377
|
+
if (days <= CERT_MIN_DAYS) {
|
|
6378
|
+
return { result: "fail", checkedAt: at, note: `TLS cert expires in ${days}d` };
|
|
6379
|
+
}
|
|
6380
|
+
return { result: "pass", checkedAt: at, note: `Custom domain, valid cert (${days}d left)` };
|
|
6381
|
+
}
|
|
6382
|
+
|
|
5814
6383
|
// src/reports/draft.ts
|
|
5815
6384
|
init_attachments();
|
|
5816
6385
|
|
|
@@ -5829,7 +6398,7 @@ import { readFileSync as readFileSync3 } from "fs";
|
|
|
5829
6398
|
import { JWT } from "google-auth-library";
|
|
5830
6399
|
import { BetaAnalyticsDataClient } from "@google-analytics/data";
|
|
5831
6400
|
var ANALYTICS_READONLY = "https://www.googleapis.com/auth/analytics.readonly";
|
|
5832
|
-
var
|
|
6401
|
+
var MS_PER_DAY3 = 864e5;
|
|
5833
6402
|
function ymd2(d) {
|
|
5834
6403
|
return d.toISOString().slice(0, 10);
|
|
5835
6404
|
}
|
|
@@ -5842,9 +6411,9 @@ async function fetchPeriodUsers(query, periodStart, periodEnd) {
|
|
|
5842
6411
|
subject: query.subject
|
|
5843
6412
|
});
|
|
5844
6413
|
const client = new BetaAnalyticsDataClient({ authClient });
|
|
5845
|
-
const lengthDays = Math.round((periodEnd.getTime() - periodStart.getTime()) /
|
|
5846
|
-
const prevEnd = new Date(periodStart.getTime() -
|
|
5847
|
-
const prevStart = new Date(prevEnd.getTime() - lengthDays *
|
|
6414
|
+
const lengthDays = Math.round((periodEnd.getTime() - periodStart.getTime()) / MS_PER_DAY3);
|
|
6415
|
+
const prevEnd = new Date(periodStart.getTime() - MS_PER_DAY3);
|
|
6416
|
+
const prevStart = new Date(prevEnd.getTime() - lengthDays * MS_PER_DAY3);
|
|
5848
6417
|
const property = `properties/${query.propertyId}`;
|
|
5849
6418
|
const run = async (start, end) => {
|
|
5850
6419
|
const [resp] = await client.runReport({
|
|
@@ -5968,7 +6537,7 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
5968
6537
|
const periodStart = base !== null ? await derivePeriodStart(base, siteRow, reportType, today) : daysAgo(today, 30);
|
|
5969
6538
|
const periodEnd = today;
|
|
5970
6539
|
const completedOn = today;
|
|
5971
|
-
const lastTestedDate = reportType === "Maintenance" && siteRow.
|
|
6540
|
+
const lastTestedDate = reportType === "Maintenance" && siteRow.lastLighthouseAuditAt ? new Date(siteRow.lastLighthouseAuditAt) : null;
|
|
5972
6541
|
const gaResult = base !== null ? await fetchGaUsers(siteRow, periodStart, periodEnd) : NO_ENRICHMENT;
|
|
5973
6542
|
const searchResult = base !== null ? await fetchSearch(siteRow, periodStart, periodEnd) : NO_ENRICHMENT;
|
|
5974
6543
|
const gaUsers = gaResult.value;
|
|
@@ -6015,6 +6584,9 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
6015
6584
|
supersededIds: outcome2.supersededIds
|
|
6016
6585
|
};
|
|
6017
6586
|
}
|
|
6587
|
+
const evidence = autoTickChecklist(siteRow, reportType, completedOn, { search: searchResult });
|
|
6588
|
+
const checklistTicks = [...evidence.entries()].filter(([, e]) => e.result === "pass").map(([field]) => field);
|
|
6589
|
+
const autoEvidence = Object.fromEntries(evidence);
|
|
6018
6590
|
const reportId = `${siteRow.name} \u2014 ${reportType} \u2014 ${periodEnd.toISOString().slice(0, 10)}`;
|
|
6019
6591
|
const created = await createDraft(base, {
|
|
6020
6592
|
reportId,
|
|
@@ -6028,7 +6600,9 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
6028
6600
|
lastTestedDate,
|
|
6029
6601
|
...gaUsers ? { gaUsersCurrent: gaUsers.current, gaUsersPrevious: gaUsers.previous } : {},
|
|
6030
6602
|
...search ? { searchFoundPage1: search.foundOnPage1 } : {},
|
|
6031
|
-
...search?.foundOnPage1 && search.position !== null ? { searchPosition: search.position } : {}
|
|
6603
|
+
...search?.foundOnPage1 && search.position !== null ? { searchPosition: search.position } : {},
|
|
6604
|
+
checklistTicks,
|
|
6605
|
+
autoEvidence
|
|
6032
6606
|
});
|
|
6033
6607
|
await uploadDraftHtml(created.id, slug, periodEnd, html);
|
|
6034
6608
|
const outcome = await queueDraft(base, {
|
|
@@ -6859,7 +7433,9 @@ var AUDIT_DESCRIPTIONS = {
|
|
|
6859
7433
|
lighthouse: "Run @lhci/cli autorun using the canonical lighthouserc.",
|
|
6860
7434
|
a11y: "Playwright + axe against the canonical a11y routes.",
|
|
6861
7435
|
security: "pnpm audit (falls back to npm audit), prod-deps by default.",
|
|
6862
|
-
lint: "ESLint + Prettier using the canonical configs."
|
|
7436
|
+
lint: "ESLint + Prettier using the canonical configs.",
|
|
7437
|
+
domain: "DNS resolve + TLS cert expiry against the deployed URL (checkout-free).",
|
|
7438
|
+
browser: "Playwright across desktop engines + mobile devices + link-check against the deployed URL (checkout-free)."
|
|
6863
7439
|
};
|
|
6864
7440
|
var RECIPE_DESCRIPTIONS = {
|
|
6865
7441
|
"sync-configs": "Overwrite a site's canonical configs to match @reddoorla/maintenance.",
|