@reddoorla/maintenance 0.68.0 → 0.71.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/airtable-2PJ4NSAV.js +9 -0
- package/dist/{announce-72R3LRNT.js → announce-3JE5FKW7.js} +6 -6
- package/dist/blux-FKZYMKLF.js +2205 -0
- package/dist/blux-FKZYMKLF.js.map +1 -0
- package/dist/{bump-deps-QNM3H4GI.js → bump-deps-K3JMLMBB.js} +3 -2
- package/dist/bump-deps-K3JMLMBB.js.map +1 -0
- package/dist/{chunk-RINXPU3H.js → chunk-2PWQ6MPG.js} +2 -2
- package/dist/{chunk-H6YZBXBQ.js → chunk-52TDNN7K.js} +37 -15
- package/dist/chunk-52TDNN7K.js.map +1 -0
- package/dist/{chunk-XWPZ46IN.js → chunk-5QC5KJHR.js} +2 -2
- package/dist/{chunk-O6G4HHMF.js → chunk-6J25IIAI.js} +162 -42
- package/dist/chunk-6J25IIAI.js.map +1 -0
- package/dist/chunk-7MJBTV2R.js +145 -0
- package/dist/chunk-7MJBTV2R.js.map +1 -0
- package/dist/{chunk-6JFOAPW4.js → chunk-AYM2EFSK.js} +38 -6
- package/dist/chunk-AYM2EFSK.js.map +1 -0
- package/dist/{chunk-GKSXZIMX.js → chunk-CMQMNFFJ.js} +77 -3
- package/dist/chunk-CMQMNFFJ.js.map +1 -0
- package/dist/{chunk-6MT5W6UF.js → chunk-F6OE6MGQ.js} +6 -3
- package/dist/chunk-F6OE6MGQ.js.map +1 -0
- package/dist/{chunk-OTBA7DON.js → chunk-J2SHWT2O.js} +1 -1
- package/dist/chunk-J2SHWT2O.js.map +1 -0
- package/dist/{chunk-XVR7UM6Z.js → chunk-JNXCDUYB.js} +4 -4
- package/dist/{chunk-2E2X5BCG.js → chunk-KOAGMTRI.js} +3 -3
- package/dist/chunk-KRDLHAKK.js +17 -0
- package/dist/chunk-KRDLHAKK.js.map +1 -0
- package/dist/{chunk-LYCKN7ZR.js → chunk-LPMQXS2N.js} +8 -4
- package/dist/chunk-LPMQXS2N.js.map +1 -0
- package/dist/{chunk-WCN54744.js → chunk-M7HOQ4W7.js} +388 -8
- package/dist/chunk-M7HOQ4W7.js.map +1 -0
- package/dist/{chunk-LCKKIJ22.js → chunk-MPRLMGJU.js} +11 -3
- package/dist/chunk-MPRLMGJU.js.map +1 -0
- package/dist/chunk-PKVZMFEB.js +315 -0
- package/dist/chunk-PKVZMFEB.js.map +1 -0
- package/dist/{chunk-A5KYRGQA.js → chunk-PSJQE6T6.js} +4 -4
- package/dist/chunk-SL24DCGC.js +41 -0
- package/dist/chunk-SL24DCGC.js.map +1 -0
- package/dist/{chunk-MAYHLXCS.js → chunk-VR246V3G.js} +25 -5
- package/dist/chunk-VR246V3G.js.map +1 -0
- package/dist/{chunk-45AUGDFR.js → chunk-WN4NEHE7.js} +2 -7
- package/dist/chunk-WN4NEHE7.js.map +1 -0
- package/dist/{chunk-YQMYQAJX.js → chunk-Y7LZHPDF.js} +2 -2
- package/dist/{chunk-C3PDS623.js → chunk-YWRQEDB3.js} +53 -3
- package/dist/chunk-YWRQEDB3.js.map +1 -0
- package/dist/cli/bin.js +63 -19
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +1 -1
- package/dist/cli/commands/audit.js +13 -11
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client/index.d.ts +84 -0
- package/dist/client/index.js +47 -0
- package/dist/client/index.js.map +1 -0
- package/dist/configs/playwright-a11y.js +1 -1
- package/dist/{convert-to-pnpm-XH7NYRVK.js → convert-to-pnpm-6F4RAFEJ.js} +3 -2
- package/dist/convert-to-pnpm-6F4RAFEJ.js.map +1 -0
- package/dist/{digest-J7TDG62Q.js → digest-GZCZYJKC.js} +11 -11
- package/dist/{ensure-site-2PF522VL.js → ensure-site-H5VMBPLF.js} +2 -2
- package/dist/forms/index.d.ts +2 -0
- package/dist/forms/index.js +7 -1
- package/dist/forms/index.js.map +1 -1
- package/dist/{github-signals-PP7GWYIY.js → github-signals-SQECQNST.js} +8 -8
- package/dist/health-endpoint-QXWM6GW2.js +54 -0
- package/dist/health-endpoint-QXWM6GW2.js.map +1 -0
- package/dist/index.d.ts +132 -17
- package/dist/index.js +213 -70
- package/dist/index.js.map +1 -1
- package/dist/{init-WLR35YGN.js → init-KQOIPLVT.js} +12 -8
- package/dist/init-KQOIPLVT.js.map +1 -0
- package/dist/{launch-2HPQXWII.js → launch-RX4VI7F2.js} +13 -13
- package/dist/{lighthouse-airtable-UBSXKHAQ.js → lighthouse-airtable-TTXULEGU.js} +3 -3
- package/dist/{onboard-X6KJ5Z2V.js → onboard-TFBL62J3.js} +2 -2
- package/dist/{orchestrate-PVCGBH4K.js → orchestrate-DYA4XAQJ.js} +6 -6
- package/dist/{preflight-KWVYJ2DU.js → preflight-S3GYME3Q.js} +8 -8
- package/dist/probe-DJAMLVTT.js +46 -0
- package/dist/probe-DJAMLVTT.js.map +1 -0
- package/dist/recipes/sync-configs.d.ts +1 -1
- package/dist/recipes/sync-configs.js +2 -2
- package/dist/{renovate-dispatch-LG4RJGDY.js → renovate-dispatch-MZPAWVBL.js} +2 -2
- package/dist/{report-ZBFMTT76.js → report-BLZ67QAQ.js} +12 -12
- package/dist/run-migration-LJNNZWIV.js +205 -0
- package/dist/run-migration-LJNNZWIV.js.map +1 -0
- package/dist/{self-updating-JTTFOJ7Q.js → self-updating-GSH427XP.js} +2 -2
- package/dist/{selftest-LFZPZRJP.js → selftest-KPTL6KC4.js} +11 -11
- package/dist/smoke-suite-GP3W57JP.js +54 -0
- package/dist/smoke-suite-GP3W57JP.js.map +1 -0
- package/dist/{svelte-codemods-KJLNH6FQ.js → svelte-codemods-GYZZX742.js} +3 -2
- package/dist/svelte-codemods-GYZZX742.js.map +1 -0
- package/dist/{sync-configs-6GBZLGRQ.js → sync-configs-CQNOTJ2B.js} +5 -4
- package/dist/sync-configs-CQNOTJ2B.js.map +1 -0
- package/dist/{types-Dg4mHvyf.d.ts → types-0jreMCF5.d.ts} +2 -2
- package/dist/{upgrade-TPTE26FG.js → upgrade-RTAJZD56.js} +3 -2
- package/dist/upgrade-RTAJZD56.js.map +1 -0
- package/dist/{websites-6YTT5ZTO.js → websites-SVI4KYSH.js} +8 -2
- package/dist/{write-audits-to-airtable-IWOO4FFH.js → write-audits-to-airtable-3XFNWMUE.js} +4 -4
- package/package.json +8 -1
- package/dist/airtable-WGQQ3KAD.js +0 -9
- package/dist/bump-deps-QNM3H4GI.js.map +0 -1
- package/dist/chunk-45AUGDFR.js.map +0 -1
- package/dist/chunk-6JFOAPW4.js.map +0 -1
- package/dist/chunk-6MT5W6UF.js.map +0 -1
- package/dist/chunk-C3PDS623.js.map +0 -1
- package/dist/chunk-GKSXZIMX.js.map +0 -1
- package/dist/chunk-H6YZBXBQ.js.map +0 -1
- package/dist/chunk-LCKKIJ22.js.map +0 -1
- package/dist/chunk-LYCKN7ZR.js.map +0 -1
- package/dist/chunk-MAYHLXCS.js.map +0 -1
- package/dist/chunk-O6G4HHMF.js.map +0 -1
- package/dist/chunk-OTBA7DON.js.map +0 -1
- package/dist/chunk-WCN54744.js.map +0 -1
- package/dist/convert-to-pnpm-XH7NYRVK.js.map +0 -1
- package/dist/init-WLR35YGN.js.map +0 -1
- package/dist/svelte-codemods-KJLNH6FQ.js.map +0 -1
- package/dist/sync-configs-6GBZLGRQ.js.map +0 -1
- package/dist/upgrade-TPTE26FG.js.map +0 -1
- /package/dist/{airtable-WGQQ3KAD.js.map → airtable-2PJ4NSAV.js.map} +0 -0
- /package/dist/{announce-72R3LRNT.js.map → announce-3JE5FKW7.js.map} +0 -0
- /package/dist/{chunk-RINXPU3H.js.map → chunk-2PWQ6MPG.js.map} +0 -0
- /package/dist/{chunk-XWPZ46IN.js.map → chunk-5QC5KJHR.js.map} +0 -0
- /package/dist/{chunk-XVR7UM6Z.js.map → chunk-JNXCDUYB.js.map} +0 -0
- /package/dist/{chunk-2E2X5BCG.js.map → chunk-KOAGMTRI.js.map} +0 -0
- /package/dist/{chunk-A5KYRGQA.js.map → chunk-PSJQE6T6.js.map} +0 -0
- /package/dist/{chunk-YQMYQAJX.js.map → chunk-Y7LZHPDF.js.map} +0 -0
- /package/dist/{digest-J7TDG62Q.js.map → digest-GZCZYJKC.js.map} +0 -0
- /package/dist/{ensure-site-2PF522VL.js.map → ensure-site-H5VMBPLF.js.map} +0 -0
- /package/dist/{github-signals-PP7GWYIY.js.map → github-signals-SQECQNST.js.map} +0 -0
- /package/dist/{launch-2HPQXWII.js.map → launch-RX4VI7F2.js.map} +0 -0
- /package/dist/{lighthouse-airtable-UBSXKHAQ.js.map → lighthouse-airtable-TTXULEGU.js.map} +0 -0
- /package/dist/{onboard-X6KJ5Z2V.js.map → onboard-TFBL62J3.js.map} +0 -0
- /package/dist/{orchestrate-PVCGBH4K.js.map → orchestrate-DYA4XAQJ.js.map} +0 -0
- /package/dist/{preflight-KWVYJ2DU.js.map → preflight-S3GYME3Q.js.map} +0 -0
- /package/dist/{renovate-dispatch-LG4RJGDY.js.map → renovate-dispatch-MZPAWVBL.js.map} +0 -0
- /package/dist/{report-ZBFMTT76.js.map → report-BLZ67QAQ.js.map} +0 -0
- /package/dist/{self-updating-JTTFOJ7Q.js.map → self-updating-GSH427XP.js.map} +0 -0
- /package/dist/{selftest-LFZPZRJP.js.map → selftest-KPTL6KC4.js.map} +0 -0
- /package/dist/{websites-6YTT5ZTO.js.map → websites-SVI4KYSH.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-IWOO4FFH.js.map → write-audits-to-airtable-3XFNWMUE.js.map} +0 -0
|
@@ -8,19 +8,19 @@ import {
|
|
|
8
8
|
} from "../../chunk-4NAOR44V.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveSites
|
|
11
|
-
} from "../../chunk-
|
|
11
|
+
} from "../../chunk-Y7LZHPDF.js";
|
|
12
12
|
import {
|
|
13
13
|
ALL_AUDIT_NAMES,
|
|
14
14
|
runOneAudit
|
|
15
|
-
} from "../../chunk-
|
|
15
|
+
} from "../../chunk-M7HOQ4W7.js";
|
|
16
|
+
import "../../chunk-GASBX52O.js";
|
|
17
|
+
import "../../chunk-YEGHUODU.js";
|
|
16
18
|
import "../../chunk-LKGVSM2O.js";
|
|
17
19
|
import {
|
|
18
20
|
recordFleetEventsBestEffort
|
|
19
|
-
} from "../../chunk-
|
|
21
|
+
} from "../../chunk-J2SHWT2O.js";
|
|
20
22
|
import "../../chunk-PH3AGF4Z.js";
|
|
21
23
|
import "../../chunk-HIN3P4ZN.js";
|
|
22
|
-
import "../../chunk-GASBX52O.js";
|
|
23
|
-
import "../../chunk-YEGHUODU.js";
|
|
24
24
|
import "../../chunk-FDIFTIXC.js";
|
|
25
25
|
import "../../chunk-HVOOCK6L.js";
|
|
26
26
|
import "../../chunk-XXTZBPUY.js";
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "../../chunk-U5SZ4FOP.js";
|
|
31
31
|
import "../../chunk-6VAL7XJT.js";
|
|
32
32
|
import "../../chunk-EYODPUHV.js";
|
|
33
|
-
import "../../chunk-
|
|
33
|
+
import "../../chunk-LPMQXS2N.js";
|
|
34
34
|
|
|
35
35
|
// src/cli/commands/audit.ts
|
|
36
36
|
import { resolve } from "path";
|
|
@@ -159,7 +159,9 @@ var CHECKOUT_FREE_AUDITS = /* @__PURE__ */ new Set([
|
|
|
159
159
|
"lighthouse",
|
|
160
160
|
"domain",
|
|
161
161
|
"browser",
|
|
162
|
-
"netlify-deploy"
|
|
162
|
+
"netlify-deploy",
|
|
163
|
+
"function-health",
|
|
164
|
+
"form-e2e"
|
|
163
165
|
]);
|
|
164
166
|
var NETLIFY_ID_AUDITS = /* @__PURE__ */ new Set(["netlify-deploy"]);
|
|
165
167
|
function auditNeedsCheckout(site, which) {
|
|
@@ -231,9 +233,9 @@ ${skipNotice}`;
|
|
|
231
233
|
let writeBackFailed = false;
|
|
232
234
|
if (opts.writeAirtable !== void 0) {
|
|
233
235
|
const { openBase, readAirtableConfig } = await import("../../client-EPVO3FEH.js");
|
|
234
|
-
const { listWebsites } = await import("../../websites-
|
|
236
|
+
const { listWebsites } = await import("../../websites-SVI4KYSH.js");
|
|
235
237
|
if (opts.fleet !== void 0) {
|
|
236
|
-
const { writeFleetAuditsToAirtable, formatFleetWriteSummary } = await import("../../write-audits-to-airtable-
|
|
238
|
+
const { writeFleetAuditsToAirtable, formatFleetWriteSummary } = await import("../../write-audits-to-airtable-3XFNWMUE.js");
|
|
237
239
|
const base = openBase(readAirtableConfig());
|
|
238
240
|
const websites = await listWebsites(base);
|
|
239
241
|
const fleetWrite = await writeFleetAuditsToAirtable({ base, websites, results });
|
|
@@ -249,8 +251,8 @@ ${skipNotice}`;
|
|
|
249
251
|
|
|
250
252
|
${formatFleetWriteSummary(fleetWrite)}`;
|
|
251
253
|
} else {
|
|
252
|
-
const { resolveSlugFromCwd } = await import("../../lighthouse-airtable-
|
|
253
|
-
const { writeAuditsToAirtable } = await import("../../write-audits-to-airtable-
|
|
254
|
+
const { resolveSlugFromCwd } = await import("../../lighthouse-airtable-TTXULEGU.js");
|
|
255
|
+
const { writeAuditsToAirtable } = await import("../../write-audits-to-airtable-3XFNWMUE.js");
|
|
254
256
|
const slug = typeof opts.writeAirtable === "string" && opts.writeAirtable.length > 0 ? opts.writeAirtable : await resolveSlugFromCwd(cwd);
|
|
255
257
|
let writeSummary = null;
|
|
256
258
|
await new Listr(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/cli/commands/audit.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { Listr } from \"listr2\";\nimport { runOneAudit, ALL_AUDIT_NAMES } from \"../../audits/index.js\";\nimport type { AuditName, AuditResult, Site } from \"../../types.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\nimport {\n prepareFleetSites,\n formatSkippedNotice,\n type SkippedSite,\n} from \"../fleet/prepare-sites.js\";\nimport { isHttpUrl } from \"../../util/url.js\";\nimport { fleetWorkdir } from \"../../util/fleet-workdir.js\";\nimport { recordFleetEventsBestEffort } from \"../../audits/fleet-events-writer.js\";\nimport { fleetSweptEvent } from \"../../audits/fleet-event-detectors.js\";\n\nexport type AuditCommandOptions = {\n only?: string;\n json?: boolean;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n /**\n * After running, push the lighthouse scores to the matching Websites row\n * in Airtable. `true` (no value) = derive slug from cwd/package.json#name;\n * string = explicit slug (e.g. \"med-solutions-of-texas\").\n */\n writeAirtable?: string | boolean;\n /** Exit non-zero if any a11y violations are found (overrides warn). For CI gates. */\n failOnViolations?: boolean;\n /** Audit this deployed URL directly (lighthouse only; single-site). */\n url?: string;\n /** Max sites to audit in parallel (fleet mode). Unset = all at once;\n * `1` = sequential (used by the nightly CI workflow). */\n concurrency?: string;\n};\n\nfunction parseOnly(value: string | undefined): AuditName[] | undefined {\n if (!value) return undefined;\n const names = value.split(\",\").map((s) => s.trim());\n for (const n of names) {\n if (!ALL_AUDIT_NAMES.includes(n as AuditName)) {\n throw Object.assign(new Error(`unknown audit in --only: ${n}`), { exitCode: 2 });\n }\n }\n return names as AuditName[];\n}\n\nfunction formatTable(results: AuditResult[]): string {\n return results\n .map((r) => `${r.audit.padEnd(12)} ${r.status.padEnd(5)} ${r.site}\\n ${r.summary}`)\n .join(\"\\n\");\n}\n\nexport function auditExitCode(results: AuditResult[], failOnViolations: boolean): number {\n if (results.some((r) => r.status === \"fail\")) return 1;\n if (failOnViolations) {\n const a11yViolations = results\n .filter((r) => r.audit === \"a11y\")\n .reduce(\n (n, r) =>\n n + ((r.details as { totalViolations?: number } | undefined)?.totalViolations ?? 0),\n 0,\n );\n if (a11yViolations > 0) return 1;\n }\n return 0;\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1_000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1_000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const m = Math.floor(totalSeconds / 60);\n const s = totalSeconds % 60;\n return `${m}m${s.toString().padStart(2, \"0\")}s`;\n}\n\ntype Renderer = \"default\" | \"silent\";\n\n/** Parse the `--concurrency <n>` flag into a Listr `concurrent` value. Unset →\n * `true` (all sites in parallel, the interactive default). A positive integer\n * bounds how many sites audit at once — `--concurrency 1` runs sequentially,\n * which is what the nightly CI workflow uses so ~10 deployed-Lighthouse runs\n * don't saturate a 2-core runner (one flaked locally at full parallelism). */\nexport function parseConcurrency(value: string | undefined): boolean | number {\n if (value === undefined) return true;\n const n = Number(value);\n if (!Number.isInteger(n) || n < 1) {\n throw Object.assign(new Error(`--concurrency must be a positive integer, got \"${value}\"`), {\n exitCode: 2,\n });\n }\n return n;\n}\n\n/** Build the audit-progress task list. Single-site → each audit is a sibling\n * task. Fleet → each site is a task whose `output` shows X/N audits done as\n * they complete (audits-per-site still run in parallel). Results are pushed\n * into the shared `results` array; tasks throw on `fail` status so listr2\n * paints them red, but `exitOnError: false` keeps other tasks running. */\nfunction buildAuditTasks(\n sites: Site[],\n which: AuditName[],\n results: AuditResult[],\n renderer: Renderer,\n concurrency: boolean | number,\n) {\n const singleSite = sites.length === 1;\n\n if (singleSite) {\n const site = sites[0]!;\n return new Listr(\n which.map((name) => ({\n title: name,\n task: async (_ctx, task) => {\n const start = Date.now();\n const result = await runOneAudit(site, name);\n results.push(result);\n const elapsed = formatDuration(Date.now() - start);\n task.title = `${name}: ${result.summary} (${elapsed})`;\n if (result.status === \"fail\") throw new Error(result.summary);\n },\n })),\n { concurrent: true, exitOnError: false, renderer },\n );\n }\n\n return new Listr(\n sites.map((site) => {\n const label = site.name || site.path; // `||`: empty slug must fall back to path, not blank\n return {\n title: label,\n task: async (_ctx, task) => {\n const start = Date.now();\n let done = 0;\n task.output = `0/${which.length} audits`;\n const settled = await Promise.all(\n which.map(async (name) => {\n const r = await runOneAudit(site, name);\n results.push(r);\n done += 1;\n task.output = `${done}/${which.length} audits`;\n return r;\n }),\n );\n const elapsed = formatDuration(Date.now() - start);\n const failed = settled.filter((r) => r.status === \"fail\").length;\n const warned = settled.filter((r) => r.status === \"warn\").length;\n const note =\n failed > 0\n ? `${failed} failed`\n : warned > 0\n ? `${warned} warning${warned === 1 ? \"\" : \"s\"}`\n : \"all green\";\n task.title = `${label}: ${note} (${elapsed})`;\n if (failed > 0) throw new Error(`${label}: ${failed} audit(s) failed`);\n },\n };\n }),\n { concurrent: concurrency, exitOnError: false, renderer },\n );\n}\n\ntype WriteSummary = Awaited<\n ReturnType<typeof import(\"../../audits/write-audits-to-airtable.js\").writeAuditsToAirtable>\n>;\n\nfunction formatWriteSummary(summary: WriteSummary): string {\n const lines = summary.writes.map((w) => {\n if (w.audit === \"lighthouse\") {\n const s = w.counts as {\n performance: number;\n accessibility: number;\n bestPractices: number;\n seo: number;\n };\n return ` lighthouse: P=${s.performance} A=${s.accessibility} BP=${s.bestPractices} SEO=${s.seo}`;\n }\n if (w.audit === \"a11y\") {\n return ` a11y: ${(w.counts as { violations: number }).violations} violations`;\n }\n if (w.audit === \"deps\") {\n const c = w.counts as { drifted: number; majorBehind: number };\n return ` deps: ${c.drifted} drifted (${c.majorBehind} major)`;\n }\n const c = w.counts as { critical: number; high: number; moderate: number; low: number };\n return ` security: ${c.critical}C/${c.high}H/${c.moderate}M/${c.low}L`;\n });\n return `→ wrote to Websites[${summary.siteName}]:\\n${lines.join(\"\\n\")}`;\n}\n\n/** Listr renderer choice. `--json` → silent so stdout stays clean for piping.\n * Otherwise listr's `default` renderer auto-falls back to `simple` in\n * non-TTY contexts (CI, log capture, our own integration tests). */\nfunction rendererFor(json: boolean | undefined): Renderer {\n return json ? \"silent\" : \"default\";\n}\n\n/** When `--url` is set but audits other than lighthouse also ran (those use the\n * local checkout, not the deployed URL), return a one-line operator notice;\n * null when there's nothing to warn about. Keeps the mixed-provenance result\n * table from silently confusing the operator. */\nexport function deployedUrlNotice(\n which: AuditName[],\n url: string | undefined,\n cwd: string,\n): string | null {\n if (url === undefined) return null;\n const others = which.filter((n) => n !== \"lighthouse\");\n if (others.length === 0) return null;\n return `note: --url only affects lighthouse; ${others.join(\", \")} ran against the local checkout at ${cwd}`;\n}\n\n/** Audits that need no repo checkout — they probe an external resource. lighthouse hits the\n * live URL; domain probes DNS/TLS of the URL's host; browser drives the deployed URL;\n * netlify-deploy queries the Netlify API by site id. */\nconst CHECKOUT_FREE_AUDITS: ReadonlySet<AuditName> = new Set<AuditName>([\n \"lighthouse\",\n \"domain\",\n \"browser\",\n \"netlify-deploy\",\n]);\n\n/** Audits whose checkout-free run is keyed off `site.netlifyId` (the Netlify API) rather than\n * the deployed URL. Everything else in CHECKOUT_FREE_AUDITS reads `site.deployedUrl`. */\nconst NETLIFY_ID_AUDITS: ReadonlySet<AuditName> = new Set<AuditName>([\"netlify-deploy\"]);\n\n/** A fleet site needs a local checkout unless every requested audit is checkout-free AND the site\n * has the external resource each one runs against (`deployedUrl` for lighthouse/domain/browser,\n * `netlifyId` for netlify-deploy). */\nexport function auditNeedsCheckout(site: Site, which: AuditName[]): boolean {\n const deployedCapable = which.every((n) => {\n if (!CHECKOUT_FREE_AUDITS.has(n)) return false;\n return NETLIFY_ID_AUDITS.has(n) ? site.netlifyId !== undefined : site.deployedUrl !== undefined;\n });\n return !deployedCapable;\n}\n\n/** Apply a single-site `--url` to the resolved sites. Returns the input\n * untouched when no url is given; otherwise requires exactly one site and\n * stamps `deployedUrl` on it so the lighthouse audit takes its deployed path.\n * The `--url`+`--fleet` combination is rejected earlier in `runAuditCommand`;\n * this length guard also covers any future multi-site single-run resolver. */\nexport function applyDeployedUrl(sites: Site[], url: string | undefined): Site[] {\n if (url === undefined) return sites;\n if (sites.length !== 1) {\n throw Object.assign(\n new Error(`--url expects exactly one site, but ${sites.length} resolved.`),\n { exitCode: 2 },\n );\n }\n // Scheme-allowlist: the URL is handed straight to Chrome/lhci, so only\n // http(s) is safe (a file:///gopher:// value would be a local-file read /\n // SSRF). Same predicate the inventory paths use.\n if (!isHttpUrl(url)) {\n throw Object.assign(new Error(`--url must be an http(s) URL (got: ${JSON.stringify(url)})`), {\n exitCode: 2,\n });\n }\n return [{ ...sites[0]!, deployedUrl: url }];\n}\n\nexport async function runAuditCommand(\n site: string | undefined,\n opts: AuditCommandOptions,\n): Promise<{ output: string; code: number }> {\n const which = parseOnly(opts.only) ?? ALL_AUDIT_NAMES;\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n\n // A literal --write-airtable=<slug> is single-site (the slug names one row).\n // Boolean --write-airtable + --fleet is fine: each site's slug comes from the\n // inventory, so there's no cwd-derived-slug ambiguity.\n if (typeof opts.writeAirtable === \"string\" && opts.fleet !== undefined) {\n throw Object.assign(\n new Error(\n \"--write-airtable=<slug> is single-site; with --fleet each site's slug comes from the inventory. Use --write-airtable (no slug) + --fleet.\",\n ),\n { exitCode: 2 },\n );\n }\n\n if (opts.url !== undefined && opts.fleet !== undefined) {\n throw Object.assign(\n new Error(\n \"--url is single-site only and cannot be combined with --fleet. Audit a single site instead.\",\n ),\n { exitCode: 2 },\n );\n }\n\n let sites = await resolveSites({\n ...(site !== undefined ? { site } : {}),\n ...(opts.fleet !== undefined ? { fleet: opts.fleet } : {}),\n ...(opts.workdir !== undefined ? { workdir: opts.workdir } : {}),\n cwd,\n });\n\n sites = applyDeployedUrl(sites, opts.url);\n\n let skippedPrep: SkippedSite[] = [];\n if (opts.fleet) {\n const workdir = opts.workdir ?? fleetWorkdir();\n const prep = await prepareFleetSites(sites, {\n workdir,\n needsCheckout: (s) => auditNeedsCheckout(s, which),\n });\n sites = prep.prepared;\n skippedPrep = prep.skipped;\n }\n\n const results: AuditResult[] = [];\n const renderer = rendererFor(opts.json);\n await buildAuditTasks(sites, which, results, renderer, parseConcurrency(opts.concurrency)).run();\n\n let output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);\n\n // Surface any site that couldn't be prepared (no auditable target, clone\n // failure) — visibly, but without reding the run. One misconfigured inventory\n // row is an operator fix, not an outage; the other sites still audited and\n // wrote back. \"No silent caps\": a dropped site is never invisible.\n const skipNotice = formatSkippedNotice(skippedPrep);\n if (skipNotice && !opts.json) output += `\\n\\n${skipNotice}`;\n\n // Did any site fail to write back to Airtable? The fleet writer collects\n // per-site failures instead of throwing, so without this the command would\n // exit 0 while rows silently failed to persist — automation keying on `$?`\n // would see a clean run. (The single-site writer throws on failure, so it's\n // already non-zero via the propagated error.)\n let writeBackFailed = false;\n if (opts.writeAirtable !== undefined) {\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const { listWebsites } = await import(\"../../reports/airtable/websites.js\");\n\n if (opts.fleet !== undefined) {\n const { writeFleetAuditsToAirtable, formatFleetWriteSummary } =\n await import(\"../../audits/write-audits-to-airtable.js\");\n const base = openBase(readAirtableConfig());\n const websites = await listWebsites(base);\n const fleetWrite = await writeFleetAuditsToAirtable({ base, websites, results });\n if (fleetWrite.failed.length > 0) writeBackFailed = true;\n // Record fleet-activity events (vuln_cleared / cert_renewed rode along on each\n // WriteSummary) plus a per-sweep rollup. Best-effort: a missing Turso cred no-ops.\n const sweep = which.includes(\"security\") ? \"security\" : \"lighthouse\";\n const now = new Date();\n const auditEvents = fleetWrite.written.flatMap((w) => w.events ?? []);\n await recordFleetEventsBestEffort(\n [...auditEvents, fleetSweptEvent(sweep, fleetWrite.written.length, now.toISOString())],\n now,\n );\n // Gate on !json: the write-summary is human text; appending it after the\n // results array would corrupt `--json` output (the other notices already\n // guard this way). The write itself still happens regardless of --json.\n if (!opts.json) output += `\\n\\n${formatFleetWriteSummary(fleetWrite)}`;\n } else {\n const { resolveSlugFromCwd } = await import(\"../../audits/lighthouse-airtable.js\");\n const { writeAuditsToAirtable } = await import(\"../../audits/write-audits-to-airtable.js\");\n const slug =\n typeof opts.writeAirtable === \"string\" && opts.writeAirtable.length > 0\n ? opts.writeAirtable\n : await resolveSlugFromCwd(cwd);\n let writeSummary: WriteSummary | null = null;\n await new Listr(\n [\n {\n title: `Write to Airtable[${slug}]`,\n task: async (_ctx, task) => {\n const base = openBase(readAirtableConfig());\n task.output = \"loading Websites…\";\n const websites = await listWebsites(base);\n task.output = \"writing scores…\";\n writeSummary = await writeAuditsToAirtable({ base, websites, slug, results });\n task.title = `Wrote to Websites[${writeSummary.siteName}] (${writeSummary.writes.length} audit type${writeSummary.writes.length === 1 ? \"\" : \"s\"})`;\n },\n },\n ],\n { renderer },\n ).run();\n if (writeSummary && !opts.json) output += `\\n\\n${formatWriteSummary(writeSummary)}`;\n }\n }\n\n const notice = deployedUrlNotice(which, opts.url, cwd);\n if (notice && !opts.json) output += `\\n\\n${notice}`;\n\n const code = Math.max(\n auditExitCode(results, opts.failOnViolations === true),\n writeBackFailed ? 1 : 0,\n );\n return { output, code };\n}\n"],"mappings":"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","names":["c"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/cli/commands/audit.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { Listr } from \"listr2\";\nimport { runOneAudit, ALL_AUDIT_NAMES } from \"../../audits/index.js\";\nimport type { AuditName, AuditResult, Site } from \"../../types.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\nimport {\n prepareFleetSites,\n formatSkippedNotice,\n type SkippedSite,\n} from \"../fleet/prepare-sites.js\";\nimport { isHttpUrl } from \"../../util/url.js\";\nimport { fleetWorkdir } from \"../../util/fleet-workdir.js\";\nimport { recordFleetEventsBestEffort } from \"../../audits/fleet-events-writer.js\";\nimport { fleetSweptEvent } from \"../../audits/fleet-event-detectors.js\";\n\nexport type AuditCommandOptions = {\n only?: string;\n json?: boolean;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n /**\n * After running, push the lighthouse scores to the matching Websites row\n * in Airtable. `true` (no value) = derive slug from cwd/package.json#name;\n * string = explicit slug (e.g. \"med-solutions-of-texas\").\n */\n writeAirtable?: string | boolean;\n /** Exit non-zero if any a11y violations are found (overrides warn). For CI gates. */\n failOnViolations?: boolean;\n /** Audit this deployed URL directly (lighthouse only; single-site). */\n url?: string;\n /** Max sites to audit in parallel (fleet mode). Unset = all at once;\n * `1` = sequential (used by the nightly CI workflow). */\n concurrency?: string;\n};\n\nfunction parseOnly(value: string | undefined): AuditName[] | undefined {\n if (!value) return undefined;\n const names = value.split(\",\").map((s) => s.trim());\n for (const n of names) {\n if (!ALL_AUDIT_NAMES.includes(n as AuditName)) {\n throw Object.assign(new Error(`unknown audit in --only: ${n}`), { exitCode: 2 });\n }\n }\n return names as AuditName[];\n}\n\nfunction formatTable(results: AuditResult[]): string {\n return results\n .map((r) => `${r.audit.padEnd(12)} ${r.status.padEnd(5)} ${r.site}\\n ${r.summary}`)\n .join(\"\\n\");\n}\n\nexport function auditExitCode(results: AuditResult[], failOnViolations: boolean): number {\n if (results.some((r) => r.status === \"fail\")) return 1;\n if (failOnViolations) {\n const a11yViolations = results\n .filter((r) => r.audit === \"a11y\")\n .reduce(\n (n, r) =>\n n + ((r.details as { totalViolations?: number } | undefined)?.totalViolations ?? 0),\n 0,\n );\n if (a11yViolations > 0) return 1;\n }\n return 0;\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1_000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1_000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const m = Math.floor(totalSeconds / 60);\n const s = totalSeconds % 60;\n return `${m}m${s.toString().padStart(2, \"0\")}s`;\n}\n\ntype Renderer = \"default\" | \"silent\";\n\n/** Parse the `--concurrency <n>` flag into a Listr `concurrent` value. Unset →\n * `true` (all sites in parallel, the interactive default). A positive integer\n * bounds how many sites audit at once — `--concurrency 1` runs sequentially,\n * which is what the nightly CI workflow uses so ~10 deployed-Lighthouse runs\n * don't saturate a 2-core runner (one flaked locally at full parallelism). */\nexport function parseConcurrency(value: string | undefined): boolean | number {\n if (value === undefined) return true;\n const n = Number(value);\n if (!Number.isInteger(n) || n < 1) {\n throw Object.assign(new Error(`--concurrency must be a positive integer, got \"${value}\"`), {\n exitCode: 2,\n });\n }\n return n;\n}\n\n/** Build the audit-progress task list. Single-site → each audit is a sibling\n * task. Fleet → each site is a task whose `output` shows X/N audits done as\n * they complete (audits-per-site still run in parallel). Results are pushed\n * into the shared `results` array; tasks throw on `fail` status so listr2\n * paints them red, but `exitOnError: false` keeps other tasks running. */\nfunction buildAuditTasks(\n sites: Site[],\n which: AuditName[],\n results: AuditResult[],\n renderer: Renderer,\n concurrency: boolean | number,\n) {\n const singleSite = sites.length === 1;\n\n if (singleSite) {\n const site = sites[0]!;\n return new Listr(\n which.map((name) => ({\n title: name,\n task: async (_ctx, task) => {\n const start = Date.now();\n const result = await runOneAudit(site, name);\n results.push(result);\n const elapsed = formatDuration(Date.now() - start);\n task.title = `${name}: ${result.summary} (${elapsed})`;\n if (result.status === \"fail\") throw new Error(result.summary);\n },\n })),\n { concurrent: true, exitOnError: false, renderer },\n );\n }\n\n return new Listr(\n sites.map((site) => {\n const label = site.name || site.path; // `||`: empty slug must fall back to path, not blank\n return {\n title: label,\n task: async (_ctx, task) => {\n const start = Date.now();\n let done = 0;\n task.output = `0/${which.length} audits`;\n const settled = await Promise.all(\n which.map(async (name) => {\n const r = await runOneAudit(site, name);\n results.push(r);\n done += 1;\n task.output = `${done}/${which.length} audits`;\n return r;\n }),\n );\n const elapsed = formatDuration(Date.now() - start);\n const failed = settled.filter((r) => r.status === \"fail\").length;\n const warned = settled.filter((r) => r.status === \"warn\").length;\n const note =\n failed > 0\n ? `${failed} failed`\n : warned > 0\n ? `${warned} warning${warned === 1 ? \"\" : \"s\"}`\n : \"all green\";\n task.title = `${label}: ${note} (${elapsed})`;\n if (failed > 0) throw new Error(`${label}: ${failed} audit(s) failed`);\n },\n };\n }),\n { concurrent: concurrency, exitOnError: false, renderer },\n );\n}\n\ntype WriteSummary = Awaited<\n ReturnType<typeof import(\"../../audits/write-audits-to-airtable.js\").writeAuditsToAirtable>\n>;\n\nfunction formatWriteSummary(summary: WriteSummary): string {\n const lines = summary.writes.map((w) => {\n if (w.audit === \"lighthouse\") {\n const s = w.counts as {\n performance: number;\n accessibility: number;\n bestPractices: number;\n seo: number;\n };\n return ` lighthouse: P=${s.performance} A=${s.accessibility} BP=${s.bestPractices} SEO=${s.seo}`;\n }\n if (w.audit === \"a11y\") {\n return ` a11y: ${(w.counts as { violations: number }).violations} violations`;\n }\n if (w.audit === \"deps\") {\n const c = w.counts as { drifted: number; majorBehind: number };\n return ` deps: ${c.drifted} drifted (${c.majorBehind} major)`;\n }\n const c = w.counts as { critical: number; high: number; moderate: number; low: number };\n return ` security: ${c.critical}C/${c.high}H/${c.moderate}M/${c.low}L`;\n });\n return `→ wrote to Websites[${summary.siteName}]:\\n${lines.join(\"\\n\")}`;\n}\n\n/** Listr renderer choice. `--json` → silent so stdout stays clean for piping.\n * Otherwise listr's `default` renderer auto-falls back to `simple` in\n * non-TTY contexts (CI, log capture, our own integration tests). */\nfunction rendererFor(json: boolean | undefined): Renderer {\n return json ? \"silent\" : \"default\";\n}\n\n/** When `--url` is set but audits other than lighthouse also ran (those use the\n * local checkout, not the deployed URL), return a one-line operator notice;\n * null when there's nothing to warn about. Keeps the mixed-provenance result\n * table from silently confusing the operator. */\nexport function deployedUrlNotice(\n which: AuditName[],\n url: string | undefined,\n cwd: string,\n): string | null {\n if (url === undefined) return null;\n const others = which.filter((n) => n !== \"lighthouse\");\n if (others.length === 0) return null;\n return `note: --url only affects lighthouse; ${others.join(\", \")} ran against the local checkout at ${cwd}`;\n}\n\n/** Audits that need no repo checkout — they probe an external resource. lighthouse hits the\n * live URL; domain probes DNS/TLS of the URL's host; browser drives the deployed URL;\n * netlify-deploy queries the Netlify API by site id; function-health fetches the deployed\n * `/health` endpoint. */\nconst CHECKOUT_FREE_AUDITS: ReadonlySet<AuditName> = new Set<AuditName>([\n \"lighthouse\",\n \"domain\",\n \"browser\",\n \"netlify-deploy\",\n \"function-health\",\n \"form-e2e\",\n]);\n\n/** Audits whose checkout-free run is keyed off `site.netlifyId` (the Netlify API) rather than\n * the deployed URL. Everything else in CHECKOUT_FREE_AUDITS reads `site.deployedUrl`. */\nconst NETLIFY_ID_AUDITS: ReadonlySet<AuditName> = new Set<AuditName>([\"netlify-deploy\"]);\n\n/** A fleet site needs a local checkout unless every requested audit is checkout-free AND the site\n * has the external resource each one runs against (`deployedUrl` for lighthouse/domain/browser,\n * `netlifyId` for netlify-deploy). */\nexport function auditNeedsCheckout(site: Site, which: AuditName[]): boolean {\n const deployedCapable = which.every((n) => {\n if (!CHECKOUT_FREE_AUDITS.has(n)) return false;\n return NETLIFY_ID_AUDITS.has(n) ? site.netlifyId !== undefined : site.deployedUrl !== undefined;\n });\n return !deployedCapable;\n}\n\n/** Apply a single-site `--url` to the resolved sites. Returns the input\n * untouched when no url is given; otherwise requires exactly one site and\n * stamps `deployedUrl` on it so the lighthouse audit takes its deployed path.\n * The `--url`+`--fleet` combination is rejected earlier in `runAuditCommand`;\n * this length guard also covers any future multi-site single-run resolver. */\nexport function applyDeployedUrl(sites: Site[], url: string | undefined): Site[] {\n if (url === undefined) return sites;\n if (sites.length !== 1) {\n throw Object.assign(\n new Error(`--url expects exactly one site, but ${sites.length} resolved.`),\n { exitCode: 2 },\n );\n }\n // Scheme-allowlist: the URL is handed straight to Chrome/lhci, so only\n // http(s) is safe (a file:///gopher:// value would be a local-file read /\n // SSRF). Same predicate the inventory paths use.\n if (!isHttpUrl(url)) {\n throw Object.assign(new Error(`--url must be an http(s) URL (got: ${JSON.stringify(url)})`), {\n exitCode: 2,\n });\n }\n return [{ ...sites[0]!, deployedUrl: url }];\n}\n\nexport async function runAuditCommand(\n site: string | undefined,\n opts: AuditCommandOptions,\n): Promise<{ output: string; code: number }> {\n const which = parseOnly(opts.only) ?? ALL_AUDIT_NAMES;\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n\n // A literal --write-airtable=<slug> is single-site (the slug names one row).\n // Boolean --write-airtable + --fleet is fine: each site's slug comes from the\n // inventory, so there's no cwd-derived-slug ambiguity.\n if (typeof opts.writeAirtable === \"string\" && opts.fleet !== undefined) {\n throw Object.assign(\n new Error(\n \"--write-airtable=<slug> is single-site; with --fleet each site's slug comes from the inventory. Use --write-airtable (no slug) + --fleet.\",\n ),\n { exitCode: 2 },\n );\n }\n\n if (opts.url !== undefined && opts.fleet !== undefined) {\n throw Object.assign(\n new Error(\n \"--url is single-site only and cannot be combined with --fleet. Audit a single site instead.\",\n ),\n { exitCode: 2 },\n );\n }\n\n let sites = await resolveSites({\n ...(site !== undefined ? { site } : {}),\n ...(opts.fleet !== undefined ? { fleet: opts.fleet } : {}),\n ...(opts.workdir !== undefined ? { workdir: opts.workdir } : {}),\n cwd,\n });\n\n sites = applyDeployedUrl(sites, opts.url);\n\n let skippedPrep: SkippedSite[] = [];\n if (opts.fleet) {\n const workdir = opts.workdir ?? fleetWorkdir();\n const prep = await prepareFleetSites(sites, {\n workdir,\n needsCheckout: (s) => auditNeedsCheckout(s, which),\n });\n sites = prep.prepared;\n skippedPrep = prep.skipped;\n }\n\n const results: AuditResult[] = [];\n const renderer = rendererFor(opts.json);\n await buildAuditTasks(sites, which, results, renderer, parseConcurrency(opts.concurrency)).run();\n\n let output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);\n\n // Surface any site that couldn't be prepared (no auditable target, clone\n // failure) — visibly, but without reding the run. One misconfigured inventory\n // row is an operator fix, not an outage; the other sites still audited and\n // wrote back. \"No silent caps\": a dropped site is never invisible.\n const skipNotice = formatSkippedNotice(skippedPrep);\n if (skipNotice && !opts.json) output += `\\n\\n${skipNotice}`;\n\n // Did any site fail to write back to Airtable? The fleet writer collects\n // per-site failures instead of throwing, so without this the command would\n // exit 0 while rows silently failed to persist — automation keying on `$?`\n // would see a clean run. (The single-site writer throws on failure, so it's\n // already non-zero via the propagated error.)\n let writeBackFailed = false;\n if (opts.writeAirtable !== undefined) {\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const { listWebsites } = await import(\"../../reports/airtable/websites.js\");\n\n if (opts.fleet !== undefined) {\n const { writeFleetAuditsToAirtable, formatFleetWriteSummary } =\n await import(\"../../audits/write-audits-to-airtable.js\");\n const base = openBase(readAirtableConfig());\n const websites = await listWebsites(base);\n const fleetWrite = await writeFleetAuditsToAirtable({ base, websites, results });\n if (fleetWrite.failed.length > 0) writeBackFailed = true;\n // Record fleet-activity events (vuln_cleared / cert_renewed rode along on each\n // WriteSummary) plus a per-sweep rollup. Best-effort: a missing Turso cred no-ops.\n const sweep = which.includes(\"security\") ? \"security\" : \"lighthouse\";\n const now = new Date();\n const auditEvents = fleetWrite.written.flatMap((w) => w.events ?? []);\n await recordFleetEventsBestEffort(\n [...auditEvents, fleetSweptEvent(sweep, fleetWrite.written.length, now.toISOString())],\n now,\n );\n // Gate on !json: the write-summary is human text; appending it after the\n // results array would corrupt `--json` output (the other notices already\n // guard this way). The write itself still happens regardless of --json.\n if (!opts.json) output += `\\n\\n${formatFleetWriteSummary(fleetWrite)}`;\n } else {\n const { resolveSlugFromCwd } = await import(\"../../audits/lighthouse-airtable.js\");\n const { writeAuditsToAirtable } = await import(\"../../audits/write-audits-to-airtable.js\");\n const slug =\n typeof opts.writeAirtable === \"string\" && opts.writeAirtable.length > 0\n ? opts.writeAirtable\n : await resolveSlugFromCwd(cwd);\n let writeSummary: WriteSummary | null = null;\n await new Listr(\n [\n {\n title: `Write to Airtable[${slug}]`,\n task: async (_ctx, task) => {\n const base = openBase(readAirtableConfig());\n task.output = \"loading Websites…\";\n const websites = await listWebsites(base);\n task.output = \"writing scores…\";\n writeSummary = await writeAuditsToAirtable({ base, websites, slug, results });\n task.title = `Wrote to Websites[${writeSummary.siteName}] (${writeSummary.writes.length} audit type${writeSummary.writes.length === 1 ? \"\" : \"s\"})`;\n },\n },\n ],\n { renderer },\n ).run();\n if (writeSummary && !opts.json) output += `\\n\\n${formatWriteSummary(writeSummary)}`;\n }\n }\n\n const notice = deployedUrlNotice(which, opts.url, cwd);\n if (notice && !opts.json) output += `\\n\\n${notice}`;\n\n const code = Math.max(\n auditExitCode(results, opts.failOnViolations === true),\n writeBackFailed ? 1 : 0,\n );\n return { output, code };\n}\n"],"mappings":"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","names":["c"]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load-aware page readiness for splash screens and intro overlays.
|
|
3
|
+
*
|
|
4
|
+
* Every splash in the fleet today hides the page behind a blind `setTimeout`,
|
|
5
|
+
* which punishes fast loads (a fully-painted page sits under an opaque cover
|
|
6
|
+
* for the full timer) and can still under-shoot slow ones. `whenPageReady`
|
|
7
|
+
* replaces the timer with real signals — eager-image settlement, document
|
|
8
|
+
* load, caller-supplied promises — bracketed by a floor (`minMs`, so a brand
|
|
9
|
+
* moment never flashes) and a ceiling (`maxMs`, so a stalled resource can
|
|
10
|
+
* never wedge the splash open).
|
|
11
|
+
*
|
|
12
|
+
* Framework-free and SSR-safe: with no DOM present it resolves immediately
|
|
13
|
+
* with reason "no-dom", so it can be called unguarded from universal code.
|
|
14
|
+
* The `env` parameter exists for tests (vitest runs in a node environment);
|
|
15
|
+
* production callers omit it and the globals are used.
|
|
16
|
+
*
|
|
17
|
+
* Local structural types stand in for DOM lib types because this package
|
|
18
|
+
* compiles with `lib: ["ES2022"]` and no DOM — same approach as
|
|
19
|
+
* src/configs/svelte.ts, which declares minimal shapes to avoid a peer dep.
|
|
20
|
+
*/
|
|
21
|
+
type ListenerTarget = {
|
|
22
|
+
addEventListener(type: string, listener: () => void, options?: {
|
|
23
|
+
once?: boolean;
|
|
24
|
+
}): void;
|
|
25
|
+
};
|
|
26
|
+
type ImageLike = ListenerTarget & {
|
|
27
|
+
/** True once the load attempt is terminal — fires for both success AND error. */
|
|
28
|
+
complete: boolean;
|
|
29
|
+
};
|
|
30
|
+
type DocumentLike = ListenerTarget & {
|
|
31
|
+
readyState: string;
|
|
32
|
+
querySelectorAll(selector: string): ArrayLike<ImageLike>;
|
|
33
|
+
};
|
|
34
|
+
type WindowLike = ListenerTarget & {
|
|
35
|
+
matchMedia?(query: string): {
|
|
36
|
+
matches: boolean;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
type PageReadyEnv = {
|
|
40
|
+
document?: DocumentLike | undefined;
|
|
41
|
+
window?: WindowLike | undefined;
|
|
42
|
+
};
|
|
43
|
+
type PageReadyOptions = {
|
|
44
|
+
/** Never resolve before this many ms — keeps a splash from flashing. Default 400. */
|
|
45
|
+
minMs?: number;
|
|
46
|
+
/** Always resolve by this many ms, loaded or not. Default 2500. */
|
|
47
|
+
maxMs?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Selector for images that must settle (load OR error) before the page
|
|
50
|
+
* counts as ready. Defaults to `img[loading="eager"]` — above-the-fold
|
|
51
|
+
* imagery. Pass `false` to skip image tracking.
|
|
52
|
+
*/
|
|
53
|
+
waitForImages?: string | false;
|
|
54
|
+
/**
|
|
55
|
+
* Also wait for `document.readyState === "complete"` (the window `load`
|
|
56
|
+
* event). Default false: full load includes every iframe and third-party
|
|
57
|
+
* script, which routinely lags the meaningful first paint by seconds —
|
|
58
|
+
* eager images are usually the right signal for a splash.
|
|
59
|
+
*/
|
|
60
|
+
waitForDocument?: boolean;
|
|
61
|
+
/** Extra readiness signals (e.g. a video poster decode). Rejections count as settled. */
|
|
62
|
+
signals?: ReadonlyArray<Promise<unknown>>;
|
|
63
|
+
};
|
|
64
|
+
type PageReadyReason = "ready" | "timeout" | "no-dom";
|
|
65
|
+
/**
|
|
66
|
+
* Resolve when the page is visually ready — or at `maxMs`, whichever comes
|
|
67
|
+
* first — but never before `minMs`. The returned reason says which path won.
|
|
68
|
+
*
|
|
69
|
+
* @example Reveal a splash overlay when the hero has painted:
|
|
70
|
+
* ```ts
|
|
71
|
+
* onMount(() => {
|
|
72
|
+
* whenPageReady({ minMs: 600, maxMs: 2400 }).then(() => (isReady = true));
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
declare function whenPageReady(options?: PageReadyOptions, env?: PageReadyEnv): Promise<PageReadyReason>;
|
|
77
|
+
/**
|
|
78
|
+
* True when the user has asked the OS for reduced motion. Splash callers
|
|
79
|
+
* should collapse `minMs` to 0 and skip decorative fades when this is set.
|
|
80
|
+
* False when no DOM (SSR) or `matchMedia` is unavailable.
|
|
81
|
+
*/
|
|
82
|
+
declare function prefersReducedMotion(env?: PageReadyEnv): boolean;
|
|
83
|
+
|
|
84
|
+
export { type PageReadyEnv, type PageReadyOptions, type PageReadyReason, prefersReducedMotion, whenPageReady };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/client/page-ready.ts
|
|
2
|
+
var DEFAULT_MIN_MS = 400;
|
|
3
|
+
var DEFAULT_MAX_MS = 2500;
|
|
4
|
+
var DEFAULT_IMAGE_SELECTOR = 'img[loading="eager"]';
|
|
5
|
+
function globals() {
|
|
6
|
+
return globalThis;
|
|
7
|
+
}
|
|
8
|
+
function delay(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
10
|
+
}
|
|
11
|
+
function documentComplete(doc, win) {
|
|
12
|
+
if (doc.readyState === "complete") return Promise.resolve();
|
|
13
|
+
return new Promise((resolve) => win.addEventListener("load", () => resolve(), { once: true }));
|
|
14
|
+
}
|
|
15
|
+
function imageSettled(img) {
|
|
16
|
+
if (img.complete) return Promise.resolve();
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
img.addEventListener("load", () => resolve(), { once: true });
|
|
19
|
+
img.addEventListener("error", () => resolve(), { once: true });
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function whenPageReady(options = {}, env = {}) {
|
|
23
|
+
const doc = env.document ?? globals().document;
|
|
24
|
+
const win = env.window ?? globals().window;
|
|
25
|
+
if (!doc || !win) return Promise.resolve("no-dom");
|
|
26
|
+
const minMs = options.minMs ?? DEFAULT_MIN_MS;
|
|
27
|
+
const maxMs = options.maxMs ?? DEFAULT_MAX_MS;
|
|
28
|
+
const selector = options.waitForImages ?? DEFAULT_IMAGE_SELECTOR;
|
|
29
|
+
const waits = [];
|
|
30
|
+
if (options.waitForDocument) waits.push(documentComplete(doc, win));
|
|
31
|
+
if (selector !== false) {
|
|
32
|
+
for (const img of Array.from(doc.querySelectorAll(selector))) waits.push(imageSettled(img));
|
|
33
|
+
}
|
|
34
|
+
for (const signal of options.signals ?? []) waits.push(signal.catch(() => void 0));
|
|
35
|
+
const ready = Promise.all(waits).then(() => "ready");
|
|
36
|
+
const capped = maxMs === Infinity ? ready : Promise.race([ready, delay(maxMs).then(() => "timeout")]);
|
|
37
|
+
return Promise.all([capped, delay(minMs)]).then(([reason]) => reason);
|
|
38
|
+
}
|
|
39
|
+
function prefersReducedMotion(env = {}) {
|
|
40
|
+
const win = env.window ?? globals().window;
|
|
41
|
+
return win?.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
prefersReducedMotion,
|
|
45
|
+
whenPageReady
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/page-ready.ts"],"sourcesContent":["/**\n * Load-aware page readiness for splash screens and intro overlays.\n *\n * Every splash in the fleet today hides the page behind a blind `setTimeout`,\n * which punishes fast loads (a fully-painted page sits under an opaque cover\n * for the full timer) and can still under-shoot slow ones. `whenPageReady`\n * replaces the timer with real signals — eager-image settlement, document\n * load, caller-supplied promises — bracketed by a floor (`minMs`, so a brand\n * moment never flashes) and a ceiling (`maxMs`, so a stalled resource can\n * never wedge the splash open).\n *\n * Framework-free and SSR-safe: with no DOM present it resolves immediately\n * with reason \"no-dom\", so it can be called unguarded from universal code.\n * The `env` parameter exists for tests (vitest runs in a node environment);\n * production callers omit it and the globals are used.\n *\n * Local structural types stand in for DOM lib types because this package\n * compiles with `lib: [\"ES2022\"]` and no DOM — same approach as\n * src/configs/svelte.ts, which declares minimal shapes to avoid a peer dep.\n */\n\ntype ListenerTarget = {\n addEventListener(type: string, listener: () => void, options?: { once?: boolean }): void;\n};\n\ntype ImageLike = ListenerTarget & {\n /** True once the load attempt is terminal — fires for both success AND error. */\n complete: boolean;\n};\n\ntype DocumentLike = ListenerTarget & {\n readyState: string;\n querySelectorAll(selector: string): ArrayLike<ImageLike>;\n};\n\ntype WindowLike = ListenerTarget & {\n matchMedia?(query: string): { matches: boolean };\n};\n\nexport type PageReadyEnv = {\n document?: DocumentLike | undefined;\n window?: WindowLike | undefined;\n};\n\nexport type PageReadyOptions = {\n /** Never resolve before this many ms — keeps a splash from flashing. Default 400. */\n minMs?: number;\n /** Always resolve by this many ms, loaded or not. Default 2500. */\n maxMs?: number;\n /**\n * Selector for images that must settle (load OR error) before the page\n * counts as ready. Defaults to `img[loading=\"eager\"]` — above-the-fold\n * imagery. Pass `false` to skip image tracking.\n */\n waitForImages?: string | false;\n /**\n * Also wait for `document.readyState === \"complete\"` (the window `load`\n * event). Default false: full load includes every iframe and third-party\n * script, which routinely lags the meaningful first paint by seconds —\n * eager images are usually the right signal for a splash.\n */\n waitForDocument?: boolean;\n /** Extra readiness signals (e.g. a video poster decode). Rejections count as settled. */\n signals?: ReadonlyArray<Promise<unknown>>;\n};\n\nexport type PageReadyReason = \"ready\" | \"timeout\" | \"no-dom\";\n\nconst DEFAULT_MIN_MS = 400;\nconst DEFAULT_MAX_MS = 2500;\nconst DEFAULT_IMAGE_SELECTOR = 'img[loading=\"eager\"]';\n\nfunction globals(): PageReadyEnv {\n return globalThis as unknown as PageReadyEnv;\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));\n}\n\nfunction documentComplete(doc: DocumentLike, win: WindowLike): Promise<void> {\n if (doc.readyState === \"complete\") return Promise.resolve();\n return new Promise((resolve) => win.addEventListener(\"load\", () => resolve(), { once: true }));\n}\n\nfunction imageSettled(img: ImageLike): Promise<void> {\n if (img.complete) return Promise.resolve();\n return new Promise((resolve) => {\n img.addEventListener(\"load\", () => resolve(), { once: true });\n img.addEventListener(\"error\", () => resolve(), { once: true });\n });\n}\n\n/**\n * Resolve when the page is visually ready — or at `maxMs`, whichever comes\n * first — but never before `minMs`. The returned reason says which path won.\n *\n * @example Reveal a splash overlay when the hero has painted:\n * ```ts\n * onMount(() => {\n * whenPageReady({ minMs: 600, maxMs: 2400 }).then(() => (isReady = true));\n * });\n * ```\n */\nexport function whenPageReady(\n options: PageReadyOptions = {},\n env: PageReadyEnv = {},\n): Promise<PageReadyReason> {\n const doc = env.document ?? globals().document;\n const win = env.window ?? globals().window;\n if (!doc || !win) return Promise.resolve(\"no-dom\");\n\n const minMs = options.minMs ?? DEFAULT_MIN_MS;\n const maxMs = options.maxMs ?? DEFAULT_MAX_MS;\n const selector = options.waitForImages ?? DEFAULT_IMAGE_SELECTOR;\n\n const waits: Promise<unknown>[] = [];\n if (options.waitForDocument) waits.push(documentComplete(doc, win));\n if (selector !== false) {\n for (const img of Array.from(doc.querySelectorAll(selector))) waits.push(imageSettled(img));\n }\n for (const signal of options.signals ?? []) waits.push(signal.catch(() => undefined));\n\n const ready = Promise.all(waits).then<PageReadyReason>(() => \"ready\");\n const capped =\n maxMs === Infinity\n ? ready\n : Promise.race([ready, delay(maxMs).then<PageReadyReason>(() => \"timeout\")]);\n\n return Promise.all([capped, delay(minMs)]).then(([reason]) => reason);\n}\n\n/**\n * True when the user has asked the OS for reduced motion. Splash callers\n * should collapse `minMs` to 0 and skip decorative fades when this is set.\n * False when no DOM (SSR) or `matchMedia` is unavailable.\n */\nexport function prefersReducedMotion(env: PageReadyEnv = {}): boolean {\n const win = env.window ?? globals().window;\n return win?.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches ?? false;\n}\n"],"mappings":";AAoEA,IAAM,iBAAiB;AACvB,IAAM,iBAAiB;AACvB,IAAM,yBAAyB;AAE/B,SAAS,UAAwB;AAC/B,SAAO;AACT;AAEA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,KAAK,IAAI,GAAG,EAAE,CAAC,CAAC;AACtE;AAEA,SAAS,iBAAiB,KAAmB,KAAgC;AAC3E,MAAI,IAAI,eAAe,WAAY,QAAO,QAAQ,QAAQ;AAC1D,SAAO,IAAI,QAAQ,CAAC,YAAY,IAAI,iBAAiB,QAAQ,MAAM,QAAQ,GAAG,EAAE,MAAM,KAAK,CAAC,CAAC;AAC/F;AAEA,SAAS,aAAa,KAA+B;AACnD,MAAI,IAAI,SAAU,QAAO,QAAQ,QAAQ;AACzC,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,QAAI,iBAAiB,QAAQ,MAAM,QAAQ,GAAG,EAAE,MAAM,KAAK,CAAC;AAC5D,QAAI,iBAAiB,SAAS,MAAM,QAAQ,GAAG,EAAE,MAAM,KAAK,CAAC;AAAA,EAC/D,CAAC;AACH;AAaO,SAAS,cACd,UAA4B,CAAC,GAC7B,MAAoB,CAAC,GACK;AAC1B,QAAM,MAAM,IAAI,YAAY,QAAQ,EAAE;AACtC,QAAM,MAAM,IAAI,UAAU,QAAQ,EAAE;AACpC,MAAI,CAAC,OAAO,CAAC,IAAK,QAAO,QAAQ,QAAQ,QAAQ;AAEjD,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,WAAW,QAAQ,iBAAiB;AAE1C,QAAM,QAA4B,CAAC;AACnC,MAAI,QAAQ,gBAAiB,OAAM,KAAK,iBAAiB,KAAK,GAAG,CAAC;AAClE,MAAI,aAAa,OAAO;AACtB,eAAW,OAAO,MAAM,KAAK,IAAI,iBAAiB,QAAQ,CAAC,EAAG,OAAM,KAAK,aAAa,GAAG,CAAC;AAAA,EAC5F;AACA,aAAW,UAAU,QAAQ,WAAW,CAAC,EAAG,OAAM,KAAK,OAAO,MAAM,MAAM,MAAS,CAAC;AAEpF,QAAM,QAAQ,QAAQ,IAAI,KAAK,EAAE,KAAsB,MAAM,OAAO;AACpE,QAAM,SACJ,UAAU,WACN,QACA,QAAQ,KAAK,CAAC,OAAO,MAAM,KAAK,EAAE,KAAsB,MAAM,SAAS,CAAC,CAAC;AAE/E,SAAO,QAAQ,IAAI,CAAC,QAAQ,MAAM,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,MAAM,MAAM,MAAM;AACtE;AAOO,SAAS,qBAAqB,MAAoB,CAAC,GAAY;AACpE,QAAM,MAAM,IAAI,UAAU,QAAQ,EAAE;AACpC,SAAO,KAAK,aAAa,kCAAkC,EAAE,WAAW;AAC1E;","names":[]}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "./chunk-4NAOR44V.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveSites
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-Y7LZHPDF.js";
|
|
12
12
|
import {
|
|
13
13
|
convertToPnpm
|
|
14
14
|
} from "./chunk-2X322ZES.js";
|
|
@@ -33,6 +33,7 @@ async function runConvertToPnpmCommand(site, opts) {
|
|
|
33
33
|
let sites = await resolveSites({
|
|
34
34
|
...site !== void 0 ? { site } : {},
|
|
35
35
|
...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
|
|
36
|
+
...opts.workdir !== void 0 ? { workdir: opts.workdir } : {},
|
|
36
37
|
cwd
|
|
37
38
|
});
|
|
38
39
|
let skipped = [];
|
|
@@ -50,4 +51,4 @@ async function runConvertToPnpmCommand(site, opts) {
|
|
|
50
51
|
export {
|
|
51
52
|
runConvertToPnpmCommand
|
|
52
53
|
};
|
|
53
|
-
//# sourceMappingURL=convert-to-pnpm-
|
|
54
|
+
//# sourceMappingURL=convert-to-pnpm-6F4RAFEJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/commands/convert-to-pnpm.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { convertToPnpm } from \"../../recipes/convert-to-pnpm.js\";\nimport type { RecipeResult } from \"../../types.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\nimport { prepareFleetSites, appendSkipNotice, type SkippedSite } from \"../fleet/prepare-sites.js\";\nimport { runRecipeOverSites } from \"../fleet/run-recipe-over-sites.js\";\nimport { fleetWorkdir } from \"../../util/fleet-workdir.js\";\n\nexport type ConvertToPnpmCommandOptions = {\n fleet?: string;\n workdir?: string;\n cwd?: string;\n};\n\nfunction formatResult(r: RecipeResult): string {\n if (r.status === \"noop\") return `[${r.site}] noop: ${r.notes ?? \"\"}`;\n if (r.status === \"failed\") return `[${r.site}] failed: ${r.notes ?? \"\"}`;\n return `[${r.site}] applied: ${r.commits.length} commit(s)\\n${r.notes ?? \"\"}`;\n}\n\nexport async function runConvertToPnpmCommand(\n site: string | undefined,\n opts: ConvertToPnpmCommandOptions,\n): Promise<{ output: string; code: number }> {\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n\n let sites = await resolveSites({\n ...(site !== undefined ? { site } : {}),\n ...(opts.fleet !== undefined ? { fleet: opts.fleet } : {}),\n ...(opts.workdir !== undefined ? { workdir: opts.workdir } : {}),\n cwd,\n });\n\n let skipped: SkippedSite[] = [];\n if (opts.fleet) {\n const workdir = opts.workdir ?? fleetWorkdir();\n const prep = await prepareFleetSites(sites, { workdir });\n sites = prep.prepared;\n skipped = prep.skipped;\n }\n\n const results = await runRecipeOverSites(\"convert-to-pnpm\", sites, (s) => convertToPnpm(s));\n\n const output = results.map(formatResult).join(\"\\n\");\n const code = results.some((r) => r.status === \"failed\") ? 1 : 0;\n return { output: appendSkipNotice(output, skipped), code };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,eAAe;AAcxB,SAAS,aAAa,GAAyB;AAC7C,MAAI,EAAE,WAAW,OAAQ,QAAO,IAAI,EAAE,IAAI,WAAW,EAAE,SAAS,EAAE;AAClE,MAAI,EAAE,WAAW,SAAU,QAAO,IAAI,EAAE,IAAI,aAAa,EAAE,SAAS,EAAE;AACtE,SAAO,IAAI,EAAE,IAAI,cAAc,EAAE,QAAQ,MAAM;AAAA,EAAe,EAAE,SAAS,EAAE;AAC7E;AAEA,eAAsB,wBACpB,MACA,MAC2C;AAC3C,QAAM,MAAM,KAAK,MAAM,QAAQ,KAAK,GAAG,IAAI,QAAQ,IAAI;AAEvD,MAAI,QAAQ,MAAM,aAAa;AAAA,IAC7B,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,IACrC,GAAI,KAAK,UAAU,SAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;AAAA,IACxD,GAAI,KAAK,YAAY,SAAY,EAAE,SAAS,KAAK,QAAQ,IAAI,CAAC;AAAA,IAC9D;AAAA,EACF,CAAC;AAED,MAAI,UAAyB,CAAC;AAC9B,MAAI,KAAK,OAAO;AACd,UAAM,UAAU,KAAK,WAAW,aAAa;AAC7C,UAAM,OAAO,MAAM,kBAAkB,OAAO,EAAE,QAAQ,CAAC;AACvD,YAAQ,KAAK;AACb,cAAU,KAAK;AAAA,EACjB;AAEA,QAAM,UAAU,MAAM,mBAAmB,mBAAmB,OAAO,CAAC,MAAM,cAAc,CAAC,CAAC;AAE1F,QAAM,SAAS,QAAQ,IAAI,YAAY,EAAE,KAAK,IAAI;AAClD,QAAM,OAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,QAAQ,IAAI,IAAI;AAC9D,SAAO,EAAE,QAAQ,iBAAiB,QAAQ,OAAO,GAAG,KAAK;AAC3D;","names":[]}
|
|
@@ -9,33 +9,33 @@ import {
|
|
|
9
9
|
diffAttention,
|
|
10
10
|
readDigestState,
|
|
11
11
|
writeDigestState
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-KOAGMTRI.js";
|
|
13
|
+
import "./chunk-VR246V3G.js";
|
|
14
|
+
import "./chunk-WN4NEHE7.js";
|
|
15
15
|
import {
|
|
16
16
|
isIdempotencyConflict
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-52TDNN7K.js";
|
|
18
18
|
import "./chunk-GECESMVS.js";
|
|
19
19
|
import {
|
|
20
20
|
escapeHtml,
|
|
21
21
|
isPendingApproval,
|
|
22
22
|
listAllReports
|
|
23
|
-
} from "./chunk-
|
|
24
|
-
import
|
|
23
|
+
} from "./chunk-AYM2EFSK.js";
|
|
24
|
+
import {
|
|
25
|
+
defaultResendClient
|
|
26
|
+
} from "./chunk-WEKL2HYJ.js";
|
|
27
|
+
import "./chunk-J2SHWT2O.js";
|
|
25
28
|
import "./chunk-PH3AGF4Z.js";
|
|
26
29
|
import "./chunk-HIN3P4ZN.js";
|
|
27
30
|
import "./chunk-U5SZ4FOP.js";
|
|
28
31
|
import {
|
|
29
32
|
listWebsites,
|
|
30
33
|
siteSlug
|
|
31
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-YWRQEDB3.js";
|
|
32
35
|
import {
|
|
33
36
|
openBase,
|
|
34
37
|
readAirtableConfig
|
|
35
38
|
} from "./chunk-HN6I2XWI.js";
|
|
36
|
-
import {
|
|
37
|
-
defaultResendClient
|
|
38
|
-
} from "./chunk-WEKL2HYJ.js";
|
|
39
39
|
import "./chunk-6VAL7XJT.js";
|
|
40
40
|
|
|
41
41
|
// src/reports/digest.ts
|
|
@@ -235,4 +235,4 @@ export {
|
|
|
235
235
|
renderDigestHtml,
|
|
236
236
|
runDigest
|
|
237
237
|
};
|
|
238
|
-
//# sourceMappingURL=digest-
|
|
238
|
+
//# sourceMappingURL=digest-GZCZYJKC.js.map
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
WEBSITES_TABLE,
|
|
3
3
|
listWebsites,
|
|
4
4
|
siteSlug
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-YWRQEDB3.js";
|
|
6
6
|
import {
|
|
7
7
|
openBase,
|
|
8
8
|
readAirtableConfig
|
|
@@ -84,4 +84,4 @@ async function runEnsureSiteCommand(slug, opts) {
|
|
|
84
84
|
export {
|
|
85
85
|
runEnsureSiteCommand
|
|
86
86
|
};
|
|
87
|
-
//# sourceMappingURL=ensure-site-
|
|
87
|
+
//# sourceMappingURL=ensure-site-H5VMBPLF.js.map
|
package/dist/forms/index.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ type SubmitToIngestOptions = {
|
|
|
49
49
|
payload: SubmissionPayload;
|
|
50
50
|
/** Injectable fetch (pass SvelteKit's `event.fetch`); defaults to global fetch. */
|
|
51
51
|
fetch?: typeof fetch;
|
|
52
|
+
/** Abort budget for the central call. Default INGEST_TIMEOUT_MS. */
|
|
53
|
+
timeoutMs?: number;
|
|
52
54
|
};
|
|
53
55
|
/**
|
|
54
56
|
* Forward a submission to the dashboard ingest endpoint. Never throws — a network
|
package/dist/forms/index.js
CHANGED
|
@@ -3,17 +3,23 @@ import {
|
|
|
3
3
|
} from "../chunk-QTHKOIFJ.js";
|
|
4
4
|
|
|
5
5
|
// src/forms/client.ts
|
|
6
|
+
var INGEST_TIMEOUT_MS = 8e3;
|
|
6
7
|
async function submitToIngest(opts) {
|
|
7
8
|
const doFetch = opts.fetch ?? fetch;
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? INGEST_TIMEOUT_MS);
|
|
8
11
|
let res;
|
|
9
12
|
try {
|
|
10
13
|
res = await doFetch(opts.url, {
|
|
11
14
|
method: "POST",
|
|
12
15
|
headers: { "content-type": "application/json", "x-forms-token": opts.token },
|
|
13
|
-
body: JSON.stringify(opts.payload)
|
|
16
|
+
body: JSON.stringify(opts.payload),
|
|
17
|
+
signal: controller.signal
|
|
14
18
|
});
|
|
15
19
|
} catch (err) {
|
|
16
20
|
return { ok: false, status: 0, error: `network error: ${String(err)}` };
|
|
21
|
+
} finally {
|
|
22
|
+
clearTimeout(timer);
|
|
17
23
|
}
|
|
18
24
|
let body = null;
|
|
19
25
|
try {
|
package/dist/forms/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/forms/client.ts","../../src/forms/action.ts","../../src/forms/meta.ts","../../src/forms/endpoint.ts"],"sourcesContent":["import { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\nimport type { SubmissionMeta } from \"./meta.js\";\n\n/**\n * The JSON a fleet site forwards to the dashboard ingest endpoint. Typed fields\n * are optional; the index signature lets a site include its own extra fields\n * (e.g. `company`) which the dashboard normalizer captures into `extraFields`.\n *\n * Each typed field allows `string | undefined` (not just `string`) so a\n * `buildPayload` mapping can use the idiomatic `form.get(\"name\")?.toString()`\n * pattern under `exactOptionalPropertyTypes` without a cast — an absent field\n * and an explicit `undefined` both serialize away in the JSON body.\n */\nexport type SubmissionPayload = {\n formType?: FormType | string | undefined;\n name?: string | undefined;\n firstName?: string | undefined;\n lastName?: string | undefined;\n email?: string | undefined;\n phone?: string | undefined;\n message?: string | undefined;\n sourceUrl?: string | undefined;\n utm?: string | undefined;\n /** Reserved transport envelope (token/IP/UA); stripped centrally, never persisted. */\n _meta?: SubmissionMeta | undefined;\n [key: string]: unknown;\n};\n\nexport type IngestClientResult =\n | { ok: true; id: string }\n | { ok: false; status: number; error: string };\n\nexport type SubmitToIngestOptions = {\n /** Full ingest endpoint incl. the site slug, e.g. https://…/api/forms/reddoor */\n url: string;\n /** The shared FORMS_INGEST_TOKEN. */\n token: string;\n payload: SubmissionPayload;\n /** Injectable fetch (pass SvelteKit's `event.fetch`); defaults to global fetch. */\n fetch?: typeof fetch;\n};\n\n/**\n * Forward a submission to the dashboard ingest endpoint. Never throws — a network\n * failure or a non-2xx response is returned as `{ ok: false }` so the caller can\n * show a friendly error rather than a 500.\n */\nexport async function submitToIngest(opts: SubmitToIngestOptions): Promise<IngestClientResult> {\n const doFetch = opts.fetch ?? fetch;\n let res: Response;\n try {\n res = await doFetch(opts.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-forms-token\": opts.token },\n body: JSON.stringify(opts.payload),\n });\n } catch (err) {\n return { ok: false, status: 0, error: `network error: ${String(err)}` };\n }\n let body: unknown = null;\n try {\n body = await res.json();\n } catch {\n // non-JSON response — fall through to the error path\n }\n const obj = body && typeof body === \"object\" ? (body as Record<string, unknown>) : null;\n if (res.ok && obj && obj.ok === true) {\n return { ok: true, id: String(obj.id ?? \"\") };\n }\n const error = obj && typeof obj.error === \"string\" ? obj.error : `ingest failed (${res.status})`;\n return { ok: false, status: res.status, error };\n}\n\nexport type SubmitScreenOutOptions = {\n /** Same ingest endpoint the site already posts submissions to. */\n url: string;\n token: string;\n reason: \"honeypot\" | \"too-fast\";\n fetch?: typeof fetch;\n /** Abort budget so a slow/hung beacon can't delay the (already-successful) response. */\n timeoutMs?: number;\n};\n\n/**\n * Best-effort screen-out beacon: tells the central ingest \"a bot was screened here\"\n * (no PII) so caught-vs-delivered is observable. Never throws — a failure is returned\n * as { ok: false } and the caller ignores it (the visitor already saw success).\n */\nexport async function submitScreenOut(\n opts: SubmitScreenOutOptions,\n): Promise<{ ok: boolean; status: number }> {\n const doFetch = opts.fetch ?? fetch;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 1500);\n try {\n const res = await doFetch(opts.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-forms-token\": opts.token },\n body: JSON.stringify({ screenOut: opts.reason }),\n signal: controller.signal,\n });\n return { ok: res.ok, status: res.status };\n } catch {\n return { ok: false, status: 0 };\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport type ScreenInput = { botField?: string | null; elapsedMs?: number | null };\nexport type ScreenResult = { ok: true } | { ok: false; reason: \"honeypot\" | \"too-fast\" };\n\n/**\n * Minimum plausible fill time; faster than this reads as a bot. Kept low (800ms)\n * on purpose: a too-fast fill is dropped *silently* (the visitor sees success),\n * so a real human who happens to be quick — autofill, a short form, a returning\n * visitor — would lose their lead with no trace. Below this, a submit is\n * effectively instant (page render → fill → click → network all under ~0.8s),\n * which a human realistically never beats but a script does. The honeypot is the\n * primary bot signal; this is the secondary one, so it errs toward letting\n * borderline-fast humans through.\n */\nexport const MIN_FILL_MS = 800;\n\n/**\n * Cheap bot screen for the site action. A filled honeypot is a bot; a submission\n * faster than MIN_FILL_MS is a bot. Missing timing data (null/undefined) is NOT a\n * rejection — a prerendered/cached page can't plant a fresh timestamp, and the\n * honeypot remains the primary signal.\n *\n * Any numeric elapsed BELOW the floor — including a NEGATIVE value — is too-fast. A\n * negative elapsed time is impossible for a real fill: it means the client posted a\n * FUTURE timestamp. The earlier `>= 0` guard let that slip through (negative skipped\n * the `< MIN_FILL_MS` branch and returned ok), silently bypassing the timing gate.\n */\nexport function screenSubmission(input: ScreenInput): ScreenResult {\n if (typeof input.botField === \"string\" && input.botField.trim().length > 0) {\n return { ok: false, reason: \"honeypot\" };\n }\n if (typeof input.elapsedMs === \"number\" && input.elapsedMs < MIN_FILL_MS) {\n return { ok: false, reason: \"too-fast\" };\n }\n return { ok: true };\n}\n\nexport { SUBMISSION_FORM_TYPES, type FormType };\n","import { fail, redirect, type ActionFailure, type RequestEvent } from \"@sveltejs/kit\";\nimport {\n submitToIngest,\n screenSubmission,\n submitScreenOut,\n type SubmissionPayload,\n} from \"./client.js\";\nimport { buildSubmissionMeta } from \"./meta.js\";\n\n/** Endpoint + token for the dashboard ingest, read per-request from site env. */\nexport type IngestActionConfig = { url?: string; token?: string };\n\nexport type CreateIngestActionOptions = {\n /** Stamped onto every payload as `formType` (a SUBMISSION_FORM_TYPES value). */\n formType: string;\n /** Read at call time so SvelteKit's dynamic private env resolves per-request. */\n getConfig: () => IngestActionConfig;\n /**\n * Map this form's fields to a payload. The factory's `formType` is always\n * authoritative and cannot be overridden by `buildPayload`.\n */\n buildPayload: (form: FormData, event: RequestEvent) => SubmissionPayload;\n /** Honeypot input name. Default \"bot-field\". */\n botFieldName?: string;\n /** Hidden timestamp input name (planted in `load`). Default \"ts\". */\n tsFieldName?: string;\n /** Field carrying the Cloudflare Turnstile token. Default \"cf-turnstile-response\". */\n turnstileFieldName?: string;\n /** fail(500) copy when env vars are unset. */\n unavailableMessage?: string;\n /** fail(502) copy when the ingest endpoint rejects/errors. */\n errorMessage?: string;\n /** Injectable clock for tests. Default Date.now. */\n now?: () => number;\n /** If set, a successful OR bot-screened submission throws redirect(303, redirectTo)\n * instead of returning {success:true} (e.g. a dedicated /thank-you page). */\n redirectTo?: string;\n};\n\nexport type IngestActionData = { success: true } | ActionFailure<{ error: string }>;\n\n/**\n * Build a SvelteKit `default` form action that screens for bots, forwards the\n * submission to the dashboard ingest endpoint, and returns SvelteKit-shaped\n * results. The per-form field mapping is the only thing a site must supply.\n */\nexport function createIngestAction(\n opts: CreateIngestActionOptions,\n): (event: RequestEvent) => Promise<IngestActionData> {\n const botFieldName = opts.botFieldName ?? \"bot-field\";\n const tsFieldName = opts.tsFieldName ?? \"ts\";\n const turnstileFieldName = opts.turnstileFieldName ?? \"cf-turnstile-response\";\n const now = opts.now ?? Date.now;\n const unavailable =\n opts.unavailableMessage ?? \"This form is temporarily unavailable. Please email us directly.\";\n const failed =\n opts.errorMessage ?? \"Something went wrong sending your message. Please try again.\";\n\n return async (event) => {\n let form: FormData;\n try {\n form = await event.request.formData();\n } catch {\n console.error(`[forms-ingest] ${opts.formType}: could not parse form body`);\n return fail(400, { error: failed });\n }\n\n // Bot screen: a filled honeypot OR an implausibly fast fill is silently\n // accepted (return success, do NOT forward) so bots get no signal.\n const screen = screenSubmission({\n botField: form.get(botFieldName)?.toString() ?? null,\n elapsedMs: elapsedMs(form.get(tsFieldName), now),\n });\n if (!screen.ok) {\n // Best-effort screen-out beacon (no PII) so catch-rate is observable, then\n // succeed exactly as before — the bot/visitor still sees success.\n const cfg = opts.getConfig();\n if (cfg.url && cfg.token) {\n await submitScreenOut({\n url: cfg.url,\n token: cfg.token,\n reason: screen.reason,\n fetch: event.fetch,\n });\n }\n return succeed();\n }\n\n const { url, token } = opts.getConfig();\n if (!url || !token) {\n console.error(`[forms-ingest] config missing for formType=${opts.formType}`);\n return fail(500, { error: unavailable });\n }\n\n const result = await submitToIngest({\n url,\n token,\n fetch: event.fetch,\n payload: {\n ...opts.buildPayload(form, event),\n formType: opts.formType,\n _meta: buildSubmissionMeta(event, form.get(turnstileFieldName)?.toString()),\n },\n });\n if (!result.ok) {\n console.error(`[forms-ingest] ${opts.formType} → ${result.status}: ${result.error}`);\n return fail(502, { error: failed });\n }\n return succeed();\n };\n\n // Single success path: redirect when configured (e.g. a dedicated /thank-you\n // page), otherwise return the SvelteKit-shaped success. `redirect()` throws, so\n // the trailing `return` keeps the `{ success: true }` type.\n function succeed(): { success: true } {\n if (opts.redirectTo) redirect(303, opts.redirectTo);\n return { success: true };\n }\n}\n\n// `FormDataEntryValue` is a DOM-lib global; this package compiles with only the\n// ES2022 lib + @types/node, where it is not in scope. Derive the type from the\n// in-scope `FormData.get` return instead — same value, no DOM-lib dependency.\nfunction elapsedMs(tsRaw: ReturnType<FormData[\"get\"]>, now: () => number): number | null {\n const ts = Number(tsRaw);\n if (!Number.isFinite(ts) || ts <= 0) return null;\n // Clamp at 0: elapsed time can't be negative. A future `ts` (clock skew or a bot\n // forging one) would otherwise yield a negative value; clamping makes it read as\n // 0ms (effectively instant) so the MIN_FILL_MS gate still trips. Defense-in-depth\n // with screenSubmission, which also treats any sub-floor elapsed as too-fast.\n return Math.max(0, now() - ts);\n}\n","/** The reserved wire envelope a fleet site forwards alongside the lead fields. */\nexport type SubmissionMeta = {\n turnstileToken?: string;\n clientIp?: string;\n userAgent?: string;\n};\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v.trim() : \"\";\n}\n\n/**\n * Defensively read the reserved `_meta` envelope off an untrusted ingest payload\n * (CENTRAL side, used by the ingest handler). Keeps only non-blank string fields,\n * dropping the rest, so a bot cannot smuggle a non-string clientIp/userAgent into\n * the transient scoring path. The token/IP/UA read here are used transiently\n * (Turnstile `remoteip` + scoring) and are NEVER persisted; the token is never\n * stored. The SITE-side writer (`buildSubmissionMeta`) is added by the\n * site-factory task; `readMeta` is the single reader (there is no `parseMeta`).\n */\nexport function readMeta(payload: unknown): SubmissionMeta {\n const meta: SubmissionMeta = {};\n if (typeof payload !== \"object\" || payload === null) return meta;\n const raw = (payload as Record<string, unknown>)._meta;\n if (typeof raw !== \"object\" || raw === null) return meta;\n const m = raw as Record<string, unknown>;\n const token = str(m.turnstileToken);\n if (token) meta.turnstileToken = token;\n const ip = str(m.clientIp);\n if (ip) meta.clientIp = ip;\n const ua = str(m.userAgent);\n if (ua) meta.userAgent = ua;\n return meta;\n}\n\n/**\n * SITE-side event shape `buildSubmissionMeta` reads. Structural (not SvelteKit's\n * `RequestEvent`) so this leaf stays SDK-free; a real `RequestEvent` is\n * structurally assignable (`getClientAddress: () => string`, `request.headers`\n * is a `Headers` with a `get`).\n */\ntype MetaEvent = {\n getClientAddress?: () => string;\n request?: { headers?: { get?: (name: string) => string | null } };\n};\n\n/**\n * Build the transient `_meta` envelope a site forwards to central ingest:\n * `{ turnstileToken?, clientIp?, userAgent? }`. Returns `undefined` when no\n * field yields a value so callers can attach it unconditionally without\n * polluting the payload (an `undefined` value is dropped by `JSON.stringify`).\n * `getClientAddress` is guarded (some adapters lack a client address and can\n * throw); UA is read defensively. None of this is ever persisted.\n */\nexport function buildSubmissionMeta(\n event: MetaEvent,\n turnstileToken: string | null | undefined,\n): SubmissionMeta | undefined {\n const meta: SubmissionMeta = {};\n\n const token = typeof turnstileToken === \"string\" ? turnstileToken.trim() : \"\";\n if (token) meta.turnstileToken = token;\n\n if (typeof event.getClientAddress === \"function\") {\n try {\n const ip = event.getClientAddress();\n if (typeof ip === \"string\" && ip.trim()) meta.clientIp = ip.trim();\n } catch {\n // Some adapters have no client address and throw; drop clientIp silently.\n }\n }\n\n const ua = event.request?.headers?.get?.(\"user-agent\");\n if (typeof ua === \"string\" && ua.trim()) meta.userAgent = ua.trim();\n\n return Object.keys(meta).length > 0 ? meta : undefined;\n}\n","import { json, type RequestEvent } from \"@sveltejs/kit\";\nimport {\n submitToIngest,\n screenSubmission,\n submitScreenOut,\n type SubmissionPayload,\n} from \"./client.js\";\nimport { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\nimport type { IngestActionConfig } from \"./action.js\";\nimport { buildSubmissionMeta } from \"./meta.js\";\n\n/**\n * Options for {@link createIngestEndpoint} — the JSON sibling of\n * `createIngestAction` for client-driven forms (modals / lightboxes / fetch)\n * that POST JSON to a `+server.ts` route instead of using a form action.\n */\nexport type CreateIngestEndpointOptions = {\n /** Read at call time so SvelteKit's dynamic private env resolves per-request. */\n getConfig: () => IngestActionConfig;\n /**\n * Map the parsed JSON body to a payload. Must set `formType` UNLESS the fixed\n * `formType` option is provided (then that is authoritative and overrides it).\n */\n buildPayload: (body: Record<string, unknown>, event: RequestEvent) => SubmissionPayload;\n /** Fixed formType for single-type endpoints; omit for multi-type endpoints\n * where `buildPayload` derives formType from the body. */\n formType?: string;\n /** Honeypot field name in the JSON body. Default \"bot-field\". */\n botFieldName?: string;\n /** Field carrying the Cloudflare Turnstile token. Default \"cf-turnstile-response\". */\n turnstileFieldName?: string;\n /** json(500) copy when env vars are unset. */\n unavailableMessage?: string;\n /** json(400/502) copy for bad input / ingest failure. */\n errorMessage?: string;\n};\n\nfunction isFormType(v: unknown): v is FormType {\n return typeof v === \"string\" && (SUBMISSION_FORM_TYPES as readonly string[]).includes(v);\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === \"string\" ? v : undefined;\n}\n\n/**\n * Build a JSON `POST` handler that screens for bots, forwards the submission to\n * the dashboard ingest endpoint, and returns `{ ok }`-shaped JSON. The per-form\n * field mapping (`buildPayload`) is the only thing a site must supply. The\n * returned function is structurally a SvelteKit `RequestHandler`.\n */\nexport function createIngestEndpoint(\n opts: CreateIngestEndpointOptions,\n): (event: RequestEvent) => Promise<Response> {\n const botFieldName = opts.botFieldName ?? \"bot-field\";\n const turnstileFieldName = opts.turnstileFieldName ?? \"cf-turnstile-response\";\n const unavailable =\n opts.unavailableMessage ?? \"This form is temporarily unavailable. Please email us directly.\";\n const failed =\n opts.errorMessage ?? \"Something went wrong sending your message. Please try again.\";\n\n return async (event) => {\n let body: Record<string, unknown>;\n try {\n const parsed: unknown = await event.request.json();\n if (!parsed || typeof parsed !== \"object\") throw new Error(\"body is not an object\");\n body = parsed as Record<string, unknown>;\n } catch {\n console.error(\"[forms-ingest] could not parse JSON body\");\n return json({ ok: false, error: failed }, { status: 400 });\n }\n\n // Bot screen: honeypot only. A client POST carries no server-planted ts, and\n // screenSubmission treats a missing elapsedMs as OK. A filled honeypot is\n // silently accepted (return ok, do NOT forward) so bots get no signal.\n const screen = screenSubmission({ botField: str(body[botFieldName]) ?? null });\n if (!screen.ok) {\n // Best-effort screen-out beacon (no PII) so catch-rate is observable, then\n // return success exactly as before — the bot/visitor still sees success.\n const cfg = opts.getConfig();\n if (cfg.url && cfg.token) {\n await submitScreenOut({\n url: cfg.url,\n token: cfg.token,\n reason: screen.reason,\n fetch: event.fetch,\n });\n }\n return json({ ok: true });\n }\n\n // buildPayload runs on untrusted JSON; a careless field access (e.g.\n // `body.name.trim()` on a non-string) would otherwise escape as a 500. Treat\n // a throw as a malformed request (400), keeping the \"never 500s\" guarantee.\n let payload: SubmissionPayload;\n try {\n payload = {\n ...opts.buildPayload(body, event),\n ...(opts.formType ? { formType: opts.formType } : {}),\n _meta: buildSubmissionMeta(event, str(body[turnstileFieldName])),\n };\n } catch (err) {\n console.error(`[forms-ingest] buildPayload threw: ${String(err)}`);\n return json({ ok: false, error: failed }, { status: 400 });\n }\n if (!isFormType(payload.formType)) {\n console.error(`[forms-ingest] invalid formType: ${String(payload.formType)}`);\n return json({ ok: false, error: failed }, { status: 400 });\n }\n\n const { url, token } = opts.getConfig();\n if (!url || !token) {\n console.error(`[forms-ingest] config missing for formType=${payload.formType}`);\n return json({ ok: false, error: unavailable }, { status: 500 });\n }\n\n const result = await submitToIngest({ url, token, fetch: event.fetch, payload });\n if (!result.ok) {\n console.error(`[forms-ingest] ${payload.formType} → ${result.status}: ${result.error}`);\n return json({ ok: false, error: failed }, { status: 502 });\n }\n return json({ ok: true });\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/forms/client.ts","../../src/forms/action.ts","../../src/forms/meta.ts","../../src/forms/endpoint.ts"],"sourcesContent":["import { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\nimport type { SubmissionMeta } from \"./meta.js\";\n\n/**\n * The JSON a fleet site forwards to the dashboard ingest endpoint. Typed fields\n * are optional; the index signature lets a site include its own extra fields\n * (e.g. `company`) which the dashboard normalizer captures into `extraFields`.\n *\n * Each typed field allows `string | undefined` (not just `string`) so a\n * `buildPayload` mapping can use the idiomatic `form.get(\"name\")?.toString()`\n * pattern under `exactOptionalPropertyTypes` without a cast — an absent field\n * and an explicit `undefined` both serialize away in the JSON body.\n */\nexport type SubmissionPayload = {\n formType?: FormType | string | undefined;\n name?: string | undefined;\n firstName?: string | undefined;\n lastName?: string | undefined;\n email?: string | undefined;\n phone?: string | undefined;\n message?: string | undefined;\n sourceUrl?: string | undefined;\n utm?: string | undefined;\n /** Reserved transport envelope (token/IP/UA); stripped centrally, never persisted. */\n _meta?: SubmissionMeta | undefined;\n [key: string]: unknown;\n};\n\nexport type IngestClientResult =\n | { ok: true; id: string }\n | { ok: false; status: number; error: string };\n\nexport type SubmitToIngestOptions = {\n /** Full ingest endpoint incl. the site slug, e.g. https://…/api/forms/reddoor */\n url: string;\n /** The shared FORMS_INGEST_TOKEN. */\n token: string;\n payload: SubmissionPayload;\n /** Injectable fetch (pass SvelteKit's `event.fetch`); defaults to global fetch. */\n fetch?: typeof fetch;\n /** Abort budget for the central call. Default INGEST_TIMEOUT_MS. */\n timeoutMs?: number;\n};\n\n/** Default abort budget for the site→central ingest call. A central function hung\n * mid-deploy otherwise leaves the visitor's submit awaiting until Netlify kills\n * the SITE function at its 10s sync limit — a broken response instead of the\n * friendly error copy (espada's 2026-07-10 form-e2e warns caught exactly this).\n * 8s clears warm (~0.3s) and cold (~1-3s) central latency by a wide margin while\n * leaving the action ~2s of the 10s envelope to respond cleanly. */\nexport const INGEST_TIMEOUT_MS = 8000;\n\n/**\n * Forward a submission to the dashboard ingest endpoint. Never throws — a network\n * failure or a non-2xx response is returned as `{ ok: false }` so the caller can\n * show a friendly error rather than a 500.\n */\nexport async function submitToIngest(opts: SubmitToIngestOptions): Promise<IngestClientResult> {\n const doFetch = opts.fetch ?? fetch;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? INGEST_TIMEOUT_MS);\n let res: Response;\n try {\n res = await doFetch(opts.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-forms-token\": opts.token },\n body: JSON.stringify(opts.payload),\n signal: controller.signal,\n });\n } catch (err) {\n return { ok: false, status: 0, error: `network error: ${String(err)}` };\n } finally {\n clearTimeout(timer);\n }\n let body: unknown = null;\n try {\n body = await res.json();\n } catch {\n // non-JSON response — fall through to the error path\n }\n const obj = body && typeof body === \"object\" ? (body as Record<string, unknown>) : null;\n if (res.ok && obj && obj.ok === true) {\n return { ok: true, id: String(obj.id ?? \"\") };\n }\n const error = obj && typeof obj.error === \"string\" ? obj.error : `ingest failed (${res.status})`;\n return { ok: false, status: res.status, error };\n}\n\nexport type SubmitScreenOutOptions = {\n /** Same ingest endpoint the site already posts submissions to. */\n url: string;\n token: string;\n reason: \"honeypot\" | \"too-fast\";\n fetch?: typeof fetch;\n /** Abort budget so a slow/hung beacon can't delay the (already-successful) response. */\n timeoutMs?: number;\n};\n\n/**\n * Best-effort screen-out beacon: tells the central ingest \"a bot was screened here\"\n * (no PII) so caught-vs-delivered is observable. Never throws — a failure is returned\n * as { ok: false } and the caller ignores it (the visitor already saw success).\n */\nexport async function submitScreenOut(\n opts: SubmitScreenOutOptions,\n): Promise<{ ok: boolean; status: number }> {\n const doFetch = opts.fetch ?? fetch;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 1500);\n try {\n const res = await doFetch(opts.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-forms-token\": opts.token },\n body: JSON.stringify({ screenOut: opts.reason }),\n signal: controller.signal,\n });\n return { ok: res.ok, status: res.status };\n } catch {\n return { ok: false, status: 0 };\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport type ScreenInput = { botField?: string | null; elapsedMs?: number | null };\nexport type ScreenResult = { ok: true } | { ok: false; reason: \"honeypot\" | \"too-fast\" };\n\n/**\n * Minimum plausible fill time; faster than this reads as a bot. Kept low (800ms)\n * on purpose: a too-fast fill is dropped *silently* (the visitor sees success),\n * so a real human who happens to be quick — autofill, a short form, a returning\n * visitor — would lose their lead with no trace. Below this, a submit is\n * effectively instant (page render → fill → click → network all under ~0.8s),\n * which a human realistically never beats but a script does. The honeypot is the\n * primary bot signal; this is the secondary one, so it errs toward letting\n * borderline-fast humans through.\n */\nexport const MIN_FILL_MS = 800;\n\n/**\n * Cheap bot screen for the site action. A filled honeypot is a bot; a submission\n * faster than MIN_FILL_MS is a bot. Missing timing data (null/undefined) is NOT a\n * rejection — a prerendered/cached page can't plant a fresh timestamp, and the\n * honeypot remains the primary signal.\n *\n * Any numeric elapsed BELOW the floor — including a NEGATIVE value — is too-fast. A\n * negative elapsed time is impossible for a real fill: it means the client posted a\n * FUTURE timestamp. The earlier `>= 0` guard let that slip through (negative skipped\n * the `< MIN_FILL_MS` branch and returned ok), silently bypassing the timing gate.\n */\nexport function screenSubmission(input: ScreenInput): ScreenResult {\n if (typeof input.botField === \"string\" && input.botField.trim().length > 0) {\n return { ok: false, reason: \"honeypot\" };\n }\n if (typeof input.elapsedMs === \"number\" && input.elapsedMs < MIN_FILL_MS) {\n return { ok: false, reason: \"too-fast\" };\n }\n return { ok: true };\n}\n\nexport { SUBMISSION_FORM_TYPES, type FormType };\n","import { fail, redirect, type ActionFailure, type RequestEvent } from \"@sveltejs/kit\";\nimport {\n submitToIngest,\n screenSubmission,\n submitScreenOut,\n type SubmissionPayload,\n} from \"./client.js\";\nimport { buildSubmissionMeta } from \"./meta.js\";\n\n/** Endpoint + token for the dashboard ingest, read per-request from site env. */\nexport type IngestActionConfig = { url?: string; token?: string };\n\nexport type CreateIngestActionOptions = {\n /** Stamped onto every payload as `formType` (a SUBMISSION_FORM_TYPES value). */\n formType: string;\n /** Read at call time so SvelteKit's dynamic private env resolves per-request. */\n getConfig: () => IngestActionConfig;\n /**\n * Map this form's fields to a payload. The factory's `formType` is always\n * authoritative and cannot be overridden by `buildPayload`.\n */\n buildPayload: (form: FormData, event: RequestEvent) => SubmissionPayload;\n /** Honeypot input name. Default \"bot-field\". */\n botFieldName?: string;\n /** Hidden timestamp input name (planted in `load`). Default \"ts\". */\n tsFieldName?: string;\n /** Field carrying the Cloudflare Turnstile token. Default \"cf-turnstile-response\". */\n turnstileFieldName?: string;\n /** fail(500) copy when env vars are unset. */\n unavailableMessage?: string;\n /** fail(502) copy when the ingest endpoint rejects/errors. */\n errorMessage?: string;\n /** Injectable clock for tests. Default Date.now. */\n now?: () => number;\n /** If set, a successful OR bot-screened submission throws redirect(303, redirectTo)\n * instead of returning {success:true} (e.g. a dedicated /thank-you page). */\n redirectTo?: string;\n};\n\nexport type IngestActionData = { success: true } | ActionFailure<{ error: string }>;\n\n/**\n * Build a SvelteKit `default` form action that screens for bots, forwards the\n * submission to the dashboard ingest endpoint, and returns SvelteKit-shaped\n * results. The per-form field mapping is the only thing a site must supply.\n */\nexport function createIngestAction(\n opts: CreateIngestActionOptions,\n): (event: RequestEvent) => Promise<IngestActionData> {\n const botFieldName = opts.botFieldName ?? \"bot-field\";\n const tsFieldName = opts.tsFieldName ?? \"ts\";\n const turnstileFieldName = opts.turnstileFieldName ?? \"cf-turnstile-response\";\n const now = opts.now ?? Date.now;\n const unavailable =\n opts.unavailableMessage ?? \"This form is temporarily unavailable. Please email us directly.\";\n const failed =\n opts.errorMessage ?? \"Something went wrong sending your message. Please try again.\";\n\n return async (event) => {\n let form: FormData;\n try {\n form = await event.request.formData();\n } catch {\n console.error(`[forms-ingest] ${opts.formType}: could not parse form body`);\n return fail(400, { error: failed });\n }\n\n // Bot screen: a filled honeypot OR an implausibly fast fill is silently\n // accepted (return success, do NOT forward) so bots get no signal.\n const screen = screenSubmission({\n botField: form.get(botFieldName)?.toString() ?? null,\n elapsedMs: elapsedMs(form.get(tsFieldName), now),\n });\n if (!screen.ok) {\n // Best-effort screen-out beacon (no PII) so catch-rate is observable, then\n // succeed exactly as before — the bot/visitor still sees success.\n const cfg = opts.getConfig();\n if (cfg.url && cfg.token) {\n await submitScreenOut({\n url: cfg.url,\n token: cfg.token,\n reason: screen.reason,\n fetch: event.fetch,\n });\n }\n return succeed();\n }\n\n const { url, token } = opts.getConfig();\n if (!url || !token) {\n console.error(`[forms-ingest] config missing for formType=${opts.formType}`);\n return fail(500, { error: unavailable });\n }\n\n const result = await submitToIngest({\n url,\n token,\n fetch: event.fetch,\n payload: {\n ...opts.buildPayload(form, event),\n formType: opts.formType,\n _meta: buildSubmissionMeta(event, form.get(turnstileFieldName)?.toString()),\n },\n });\n if (!result.ok) {\n console.error(`[forms-ingest] ${opts.formType} → ${result.status}: ${result.error}`);\n return fail(502, { error: failed });\n }\n return succeed();\n };\n\n // Single success path: redirect when configured (e.g. a dedicated /thank-you\n // page), otherwise return the SvelteKit-shaped success. `redirect()` throws, so\n // the trailing `return` keeps the `{ success: true }` type.\n function succeed(): { success: true } {\n if (opts.redirectTo) redirect(303, opts.redirectTo);\n return { success: true };\n }\n}\n\n// `FormDataEntryValue` is a DOM-lib global; this package compiles with only the\n// ES2022 lib + @types/node, where it is not in scope. Derive the type from the\n// in-scope `FormData.get` return instead — same value, no DOM-lib dependency.\nfunction elapsedMs(tsRaw: ReturnType<FormData[\"get\"]>, now: () => number): number | null {\n const ts = Number(tsRaw);\n if (!Number.isFinite(ts) || ts <= 0) return null;\n // Clamp at 0: elapsed time can't be negative. A future `ts` (clock skew or a bot\n // forging one) would otherwise yield a negative value; clamping makes it read as\n // 0ms (effectively instant) so the MIN_FILL_MS gate still trips. Defense-in-depth\n // with screenSubmission, which also treats any sub-floor elapsed as too-fast.\n return Math.max(0, now() - ts);\n}\n","/** The reserved wire envelope a fleet site forwards alongside the lead fields. */\nexport type SubmissionMeta = {\n turnstileToken?: string;\n clientIp?: string;\n userAgent?: string;\n};\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v.trim() : \"\";\n}\n\n/**\n * Defensively read the reserved `_meta` envelope off an untrusted ingest payload\n * (CENTRAL side, used by the ingest handler). Keeps only non-blank string fields,\n * dropping the rest, so a bot cannot smuggle a non-string clientIp/userAgent into\n * the transient scoring path. The token/IP/UA read here are used transiently\n * (Turnstile `remoteip` + scoring) and are NEVER persisted; the token is never\n * stored. The SITE-side writer (`buildSubmissionMeta`) is added by the\n * site-factory task; `readMeta` is the single reader (there is no `parseMeta`).\n */\nexport function readMeta(payload: unknown): SubmissionMeta {\n const meta: SubmissionMeta = {};\n if (typeof payload !== \"object\" || payload === null) return meta;\n const raw = (payload as Record<string, unknown>)._meta;\n if (typeof raw !== \"object\" || raw === null) return meta;\n const m = raw as Record<string, unknown>;\n const token = str(m.turnstileToken);\n if (token) meta.turnstileToken = token;\n const ip = str(m.clientIp);\n if (ip) meta.clientIp = ip;\n const ua = str(m.userAgent);\n if (ua) meta.userAgent = ua;\n return meta;\n}\n\n/**\n * SITE-side event shape `buildSubmissionMeta` reads. Structural (not SvelteKit's\n * `RequestEvent`) so this leaf stays SDK-free; a real `RequestEvent` is\n * structurally assignable (`getClientAddress: () => string`, `request.headers`\n * is a `Headers` with a `get`).\n */\ntype MetaEvent = {\n getClientAddress?: () => string;\n request?: { headers?: { get?: (name: string) => string | null } };\n};\n\n/**\n * Build the transient `_meta` envelope a site forwards to central ingest:\n * `{ turnstileToken?, clientIp?, userAgent? }`. Returns `undefined` when no\n * field yields a value so callers can attach it unconditionally without\n * polluting the payload (an `undefined` value is dropped by `JSON.stringify`).\n * `getClientAddress` is guarded (some adapters lack a client address and can\n * throw); UA is read defensively. None of this is ever persisted.\n */\nexport function buildSubmissionMeta(\n event: MetaEvent,\n turnstileToken: string | null | undefined,\n): SubmissionMeta | undefined {\n const meta: SubmissionMeta = {};\n\n const token = typeof turnstileToken === \"string\" ? turnstileToken.trim() : \"\";\n if (token) meta.turnstileToken = token;\n\n if (typeof event.getClientAddress === \"function\") {\n try {\n const ip = event.getClientAddress();\n if (typeof ip === \"string\" && ip.trim()) meta.clientIp = ip.trim();\n } catch {\n // Some adapters have no client address and throw; drop clientIp silently.\n }\n }\n\n const ua = event.request?.headers?.get?.(\"user-agent\");\n if (typeof ua === \"string\" && ua.trim()) meta.userAgent = ua.trim();\n\n return Object.keys(meta).length > 0 ? meta : undefined;\n}\n","import { json, type RequestEvent } from \"@sveltejs/kit\";\nimport {\n submitToIngest,\n screenSubmission,\n submitScreenOut,\n type SubmissionPayload,\n} from \"./client.js\";\nimport { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\nimport type { IngestActionConfig } from \"./action.js\";\nimport { buildSubmissionMeta } from \"./meta.js\";\n\n/**\n * Options for {@link createIngestEndpoint} — the JSON sibling of\n * `createIngestAction` for client-driven forms (modals / lightboxes / fetch)\n * that POST JSON to a `+server.ts` route instead of using a form action.\n */\nexport type CreateIngestEndpointOptions = {\n /** Read at call time so SvelteKit's dynamic private env resolves per-request. */\n getConfig: () => IngestActionConfig;\n /**\n * Map the parsed JSON body to a payload. Must set `formType` UNLESS the fixed\n * `formType` option is provided (then that is authoritative and overrides it).\n */\n buildPayload: (body: Record<string, unknown>, event: RequestEvent) => SubmissionPayload;\n /** Fixed formType for single-type endpoints; omit for multi-type endpoints\n * where `buildPayload` derives formType from the body. */\n formType?: string;\n /** Honeypot field name in the JSON body. Default \"bot-field\". */\n botFieldName?: string;\n /** Field carrying the Cloudflare Turnstile token. Default \"cf-turnstile-response\". */\n turnstileFieldName?: string;\n /** json(500) copy when env vars are unset. */\n unavailableMessage?: string;\n /** json(400/502) copy for bad input / ingest failure. */\n errorMessage?: string;\n};\n\nfunction isFormType(v: unknown): v is FormType {\n return typeof v === \"string\" && (SUBMISSION_FORM_TYPES as readonly string[]).includes(v);\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === \"string\" ? v : undefined;\n}\n\n/**\n * Build a JSON `POST` handler that screens for bots, forwards the submission to\n * the dashboard ingest endpoint, and returns `{ ok }`-shaped JSON. The per-form\n * field mapping (`buildPayload`) is the only thing a site must supply. The\n * returned function is structurally a SvelteKit `RequestHandler`.\n */\nexport function createIngestEndpoint(\n opts: CreateIngestEndpointOptions,\n): (event: RequestEvent) => Promise<Response> {\n const botFieldName = opts.botFieldName ?? \"bot-field\";\n const turnstileFieldName = opts.turnstileFieldName ?? \"cf-turnstile-response\";\n const unavailable =\n opts.unavailableMessage ?? \"This form is temporarily unavailable. Please email us directly.\";\n const failed =\n opts.errorMessage ?? \"Something went wrong sending your message. Please try again.\";\n\n return async (event) => {\n let body: Record<string, unknown>;\n try {\n const parsed: unknown = await event.request.json();\n if (!parsed || typeof parsed !== \"object\") throw new Error(\"body is not an object\");\n body = parsed as Record<string, unknown>;\n } catch {\n console.error(\"[forms-ingest] could not parse JSON body\");\n return json({ ok: false, error: failed }, { status: 400 });\n }\n\n // Bot screen: honeypot only. A client POST carries no server-planted ts, and\n // screenSubmission treats a missing elapsedMs as OK. A filled honeypot is\n // silently accepted (return ok, do NOT forward) so bots get no signal.\n const screen = screenSubmission({ botField: str(body[botFieldName]) ?? null });\n if (!screen.ok) {\n // Best-effort screen-out beacon (no PII) so catch-rate is observable, then\n // return success exactly as before — the bot/visitor still sees success.\n const cfg = opts.getConfig();\n if (cfg.url && cfg.token) {\n await submitScreenOut({\n url: cfg.url,\n token: cfg.token,\n reason: screen.reason,\n fetch: event.fetch,\n });\n }\n return json({ ok: true });\n }\n\n // buildPayload runs on untrusted JSON; a careless field access (e.g.\n // `body.name.trim()` on a non-string) would otherwise escape as a 500. Treat\n // a throw as a malformed request (400), keeping the \"never 500s\" guarantee.\n let payload: SubmissionPayload;\n try {\n payload = {\n ...opts.buildPayload(body, event),\n ...(opts.formType ? { formType: opts.formType } : {}),\n _meta: buildSubmissionMeta(event, str(body[turnstileFieldName])),\n };\n } catch (err) {\n console.error(`[forms-ingest] buildPayload threw: ${String(err)}`);\n return json({ ok: false, error: failed }, { status: 400 });\n }\n if (!isFormType(payload.formType)) {\n console.error(`[forms-ingest] invalid formType: ${String(payload.formType)}`);\n return json({ ok: false, error: failed }, { status: 400 });\n }\n\n const { url, token } = opts.getConfig();\n if (!url || !token) {\n console.error(`[forms-ingest] config missing for formType=${payload.formType}`);\n return json({ ok: false, error: unavailable }, { status: 500 });\n }\n\n const result = await submitToIngest({ url, token, fetch: event.fetch, payload });\n if (!result.ok) {\n console.error(`[forms-ingest] ${payload.formType} → ${result.status}: ${result.error}`);\n return json({ ok: false, error: failed }, { status: 502 });\n }\n return json({ ok: true });\n };\n}\n"],"mappings":"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","names":[]}
|