@reddoorla/maintenance 0.11.2 → 0.12.1

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/index.d.ts CHANGED
@@ -420,4 +420,31 @@ declare function renderSiteDashboardHtml(site: WebsiteRow, reports: ReportRow[])
420
420
  */
421
421
  declare function verifyDashboardToken(provided: string | null | undefined, expected: string | null): boolean;
422
422
 
423
- export { ALL_AUDIT_NAMES, ALL_RECIPE_NAMES, type AirtableInventoryOptions, AuditName, AuditResult, BLURRED_CID, type BumpDepsOptions, type BundledImage, CHECK_CID, type ConvertToPnpmOptions, DEFAULT_INIT_STEPS, type DraftOptions, type DraftResult, type DueItem, type HeaderImage, type InitOptions, type InitResult, type InitStep, type InitStepResult, InventoryProvider, type LighthouseScores, type LocalPathOptions, type OnboardAudit, type OnboardOptions, type OrchestrateOptions, RecipeName, RecipeResult, type RenderResult, type ReportData, type ReportType, Site, type UpgradeSvelte4to5Options, a11yAudit, a11yFixturesPage, bumpDeps, convertToPnpm, depsAudit, draftReportForSite, findDueReports, fromAirtableBase, fromJsonFile, init, isRecipeName, lighthouseAudit, lintAudit, loadBundledImages, localPath, onboard, renderReportHtml, renderSiteDashboardHtml, runAudits, runAuditsAcross, securityAudit, selfCaretRange, selfPackageVersion, sendApprovedReports, svelteCodemods, upgradeSvelte4to5, verifyDashboardToken };
423
+ /**
424
+ * Render the fleet homepage as a single HTML document. Pure function:
425
+ * no Airtable access, no env reads, no I/O. The Netlify function handler
426
+ * filters the Websites rows (drops anything without a dashboardToken — those
427
+ * aren't on the Reddoor stack), sorts, and hands here. Same style vocabulary
428
+ * as renderSiteDashboardHtml so the two pages feel like one product.
429
+ */
430
+ declare function renderFleetHomeHtml(sites: WebsiteRow[]): string;
431
+
432
+ /**
433
+ * Verify an `Authorization: Basic <base64>` header against the configured
434
+ * dashboard password. Username is intentionally ignored — operators may
435
+ * type anything when the browser prompts; only the password gates entry.
436
+ *
437
+ * Returns false for any of:
438
+ * - missing/empty Authorization header
439
+ * - non-Basic auth scheme
440
+ * - malformed base64 or payload (no colon to split user:password)
441
+ * - wrong password
442
+ * - expected password missing (DASHBOARD_PASSWORD not configured)
443
+ *
444
+ * Wrong-password compare is constant-time; lengths are checked first
445
+ * (timingSafeEqual throws on mismatch, and the length itself doesn't
446
+ * leak — operator's password length is fixed per deploy).
447
+ */
448
+ declare function verifyBasicAuth(authHeader: string | null | undefined, expectedPassword: string | null): boolean;
449
+
450
+ export { ALL_AUDIT_NAMES, ALL_RECIPE_NAMES, type AirtableInventoryOptions, AuditName, AuditResult, BLURRED_CID, type BumpDepsOptions, type BundledImage, CHECK_CID, type ConvertToPnpmOptions, DEFAULT_INIT_STEPS, type DraftOptions, type DraftResult, type DueItem, type HeaderImage, type InitOptions, type InitResult, type InitStep, type InitStepResult, InventoryProvider, type LighthouseScores, type LocalPathOptions, type OnboardAudit, type OnboardOptions, type OrchestrateOptions, RecipeName, RecipeResult, type RenderResult, type ReportData, type ReportType, Site, type UpgradeSvelte4to5Options, a11yAudit, a11yFixturesPage, bumpDeps, convertToPnpm, depsAudit, draftReportForSite, findDueReports, fromAirtableBase, fromJsonFile, init, isRecipeName, lighthouseAudit, lintAudit, loadBundledImages, localPath, onboard, renderFleetHomeHtml, renderReportHtml, renderSiteDashboardHtml, runAudits, runAuditsAcross, securityAudit, selfCaretRange, selfPackageVersion, sendApprovedReports, svelteCodemods, upgradeSvelte4to5, verifyBasicAuth, verifyDashboardToken };
package/dist/index.js CHANGED
@@ -2626,11 +2626,11 @@ async function uploadAttachment(recordId, fieldName, body, filename, contentType
2626
2626
  }
2627
2627
 
2628
2628
  // src/reports/draft.ts
2629
- function scoresFromWebsite(siteRow) {
2630
- const { pScore, rScore, bpScore, seoScore } = siteRow;
2629
+ function scoresFromWebsite(siteRow2) {
2630
+ const { pScore, rScore, bpScore, seoScore } = siteRow2;
2631
2631
  if (pScore === null || rScore === null || bpScore === null || seoScore === null) {
2632
2632
  throw new Error(
2633
- `Site '${siteRow.name}' is missing one or more Lighthouse scores on the Websites row (pScore, rScore, bpScore, seoScore). Run 'reddoor-maint audit lighthouse' from the site's checkout and paste the four numbers into Airtable, then retry.`
2633
+ `Site '${siteRow2.name}' is missing one or more Lighthouse scores on the Websites row (pScore, rScore, bpScore, seoScore). Run 'reddoor-maint audit lighthouse' from the site's checkout and paste the four numbers into Airtable, then retry.`
2634
2634
  );
2635
2635
  }
2636
2636
  return { performance: pScore, accessibility: rScore, bestPractices: bpScore, seo: seoScore };
@@ -2640,18 +2640,18 @@ function daysAgo(today, n) {
2640
2640
  out.setDate(out.getDate() - n);
2641
2641
  return out;
2642
2642
  }
2643
- async function draftReportForSite(base, siteRow, reportType, options = {}) {
2644
- const scores = scoresFromWebsite(siteRow);
2643
+ async function draftReportForSite(base, siteRow2, reportType, options = {}) {
2644
+ const scores = scoresFromWebsite(siteRow2);
2645
2645
  const today = /* @__PURE__ */ new Date();
2646
- const slug = siteSlug(siteRow.name);
2647
- const periodStart = base !== null ? await derivePeriodStart(base, siteRow, reportType, today) : daysAgo(today, 30);
2646
+ const slug = siteSlug(siteRow2.name);
2647
+ const periodStart = base !== null ? await derivePeriodStart(base, siteRow2, reportType, today) : daysAgo(today, 30);
2648
2648
  const periodEnd = today;
2649
2649
  const completedOn = today;
2650
- const lastTestedDate = reportType === "Maintenance" && siteRow.testingDay ? new Date(siteRow.testingDay) : null;
2650
+ const lastTestedDate = reportType === "Maintenance" && siteRow2.testingDay ? new Date(siteRow2.testingDay) : null;
2651
2651
  const cidName = `${slug}-header`;
2652
2652
  const { html } = await renderReportHtml({
2653
- siteName: siteRow.name,
2654
- siteUrl: siteRow.url,
2653
+ siteName: siteRow2.name,
2654
+ siteUrl: siteRow2.url,
2655
2655
  reportType,
2656
2656
  completedOn,
2657
2657
  lighthouse: scores,
@@ -2668,10 +2668,10 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
2668
2668
  return { reportRow: null, htmlPath: path, html };
2669
2669
  }
2670
2670
  if (base === null) throw new Error("base required when previewOnly=false");
2671
- const reportId = `${siteRow.name} \u2014 ${reportType} \u2014 ${periodEnd.toISOString().slice(0, 10)}`;
2671
+ const reportId = `${siteRow2.name} \u2014 ${reportType} \u2014 ${periodEnd.toISOString().slice(0, 10)}`;
2672
2672
  const created = await createDraft(base, {
2673
2673
  reportId,
2674
- siteId: siteRow.id,
2674
+ siteId: siteRow2.id,
2675
2675
  reportType,
2676
2676
  periodStart,
2677
2677
  periodEnd,
@@ -2684,8 +2684,8 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
2684
2684
  await setDraftReady(base, created.id, true);
2685
2685
  return { reportRow: created, htmlPath: null, html };
2686
2686
  }
2687
- async function derivePeriodStart(base, siteRow, reportType, today) {
2688
- const prior = await listReportsForSite(base, siteRow.id);
2687
+ async function derivePeriodStart(base, siteRow2, reportType, today) {
2688
+ const prior = await listReportsForSite(base, siteRow2.id);
2689
2689
  const sameType = prior.filter((r) => r.reportType === reportType && r.periodEnd).map((r) => r.periodEnd).sort();
2690
2690
  const latest = sameType[sameType.length - 1];
2691
2691
  return latest ? new Date(latest) : daysAgo(today, 30);
@@ -3017,6 +3017,101 @@ function verifyDashboardToken(provided, expected) {
3017
3017
  if (provided.length !== expected.length) return false;
3018
3018
  return timingSafeEqual(Buffer.from(provided, "utf-8"), Buffer.from(expected, "utf-8"));
3019
3019
  }
3020
+
3021
+ // src/dashboard/fleet-render.ts
3022
+ function escapeHtml2(s) {
3023
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3024
+ }
3025
+ function safeUrl2(raw) {
3026
+ try {
3027
+ const u = new URL(raw);
3028
+ if (u.protocol === "http:" || u.protocol === "https:") return raw;
3029
+ } catch {
3030
+ }
3031
+ return "#";
3032
+ }
3033
+ function scoreCell(value) {
3034
+ const display = value === null ? "\u2014" : String(value);
3035
+ return `<td class="score">${escapeHtml2(display)}</td>`;
3036
+ }
3037
+ function siteHrefCell(site) {
3038
+ const name = escapeHtml2(site.name);
3039
+ const token = site.dashboardToken ?? "";
3040
+ const href = `/s/${escapeHtml2(siteSlug(site.name))}?t=${escapeHtml2(token)}`;
3041
+ return `<td class="site"><a href="${href}">${name}</a></td>`;
3042
+ }
3043
+ function siteRow(site) {
3044
+ return `<tr>
3045
+ ${siteHrefCell(site)}
3046
+ <td><a href="${escapeHtml2(safeUrl2(site.url))}" class="url" target="_blank" rel="noopener">${escapeHtml2(site.url)}</a></td>
3047
+ ${scoreCell(site.pScore)}
3048
+ ${scoreCell(site.rScore)}
3049
+ ${scoreCell(site.bpScore)}
3050
+ ${scoreCell(site.seoScore)}
3051
+ </tr>`;
3052
+ }
3053
+ var STYLES2 = `
3054
+ :root { color-scheme: light dark; }
3055
+ body { font: 16px/1.5 system-ui, -apple-system, sans-serif; max-width: 1100px; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; }
3056
+ @media (prefers-color-scheme: dark) { body { color: #e8e8e8; background: #111; } a { color: #6cb6ff; } }
3057
+ h1 { margin: 0 0 0.25rem; font-size: 1.75rem; }
3058
+ .meta { color: #666; margin-bottom: 2rem; }
3059
+ table { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
3060
+ th { text-align: left; padding: 0.5rem; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: #666; border-bottom: 2px solid #ddd; }
3061
+ @media (prefers-color-scheme: dark) { th { border-color: #333; } }
3062
+ td { padding: 0.65rem 0.5rem; border-bottom: 1px solid #eee; }
3063
+ @media (prefers-color-scheme: dark) { td { border-color: #2a2a2a; } }
3064
+ td.site a { font-weight: 500; }
3065
+ td.url { color: #666; font-size: 0.85rem; }
3066
+ td.score { text-align: right; font-variant-numeric: tabular-nums; min-width: 3.5rem; }
3067
+ .empty { color: #999; padding: 2rem; text-align: center; border: 1px dashed #ccc; border-radius: 6px; }
3068
+ `;
3069
+ function renderFleetHomeHtml(sites) {
3070
+ const body = sites.length === 0 ? `<div class="empty">No sites to display.</div>` : `<table>
3071
+ <thead><tr>
3072
+ <th>Site</th>
3073
+ <th>URL</th>
3074
+ <th>Perf</th>
3075
+ <th>A11y</th>
3076
+ <th>BP</th>
3077
+ <th>SEO</th>
3078
+ </tr></thead>
3079
+ <tbody>${sites.map(siteRow).join("")}</tbody>
3080
+ </table>`;
3081
+ return `<!doctype html>
3082
+ <html lang="en">
3083
+ <head>
3084
+ <meta charset="utf-8" />
3085
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
3086
+ <title>Reddoor maintenance \u2014 fleet</title>
3087
+ <style>${STYLES2}</style>
3088
+ </head>
3089
+ <body>
3090
+ <h1>Reddoor fleet</h1>
3091
+ <div class="meta">${sites.length} site${sites.length === 1 ? "" : "s"} on the Reddoor stack.</div>
3092
+ ${body}
3093
+ </body>
3094
+ </html>`;
3095
+ }
3096
+
3097
+ // src/dashboard/basic-auth.ts
3098
+ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
3099
+ function verifyBasicAuth(authHeader, expectedPassword) {
3100
+ if (!authHeader || !expectedPassword) return false;
3101
+ const match = /^basic\s+(.+)$/i.exec(authHeader.trim());
3102
+ if (!match) return false;
3103
+ let decoded;
3104
+ try {
3105
+ decoded = Buffer.from(match[1], "base64").toString("utf-8");
3106
+ } catch {
3107
+ return false;
3108
+ }
3109
+ const colonIdx = decoded.indexOf(":");
3110
+ if (colonIdx === -1) return false;
3111
+ const provided = decoded.slice(colonIdx + 1);
3112
+ if (provided.length !== expectedPassword.length) return false;
3113
+ return timingSafeEqual2(Buffer.from(provided, "utf-8"), Buffer.from(expectedPassword, "utf-8"));
3114
+ }
3020
3115
  export {
3021
3116
  ALL_AUDIT_NAMES,
3022
3117
  ALL_RECIPE_NAMES,
@@ -3039,6 +3134,7 @@ export {
3039
3134
  loadBundledImages,
3040
3135
  localPath,
3041
3136
  onboard,
3137
+ renderFleetHomeHtml,
3042
3138
  renderReportHtml,
3043
3139
  renderSiteDashboardHtml,
3044
3140
  runAudits,
@@ -3050,6 +3146,7 @@ export {
3050
3146
  svelteCodemods,
3051
3147
  syncConfigs,
3052
3148
  upgradeSvelte4to5,
3149
+ verifyBasicAuth,
3053
3150
  verifyDashboardToken
3054
3151
  };
3055
3152
  //# sourceMappingURL=index.js.map