@reddoorla/maintenance 0.94.1 → 0.95.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +31 -0
  2. package/dist/{announce-PGT4SZZB.js → announce-CLQYX5I4.js} +4 -4
  3. package/dist/{blux-NXQLMS4W.js → blux-4KNXUP2K.js} +1 -1
  4. package/dist/{bump-deps-6OR7LL4E.js → bump-deps-C5G2EFBB.js} +6 -6
  5. package/dist/{chunk-LS6QPANO.js → chunk-2LYOAB2E.js} +2 -2
  6. package/dist/{chunk-KQB2BX4V.js → chunk-2ZAZ7ETT.js} +2 -2
  7. package/dist/{chunk-XTK5VIQB.js → chunk-AVEQUR44.js} +48 -1
  8. package/dist/chunk-AVEQUR44.js.map +1 -0
  9. package/dist/{chunk-B6NUZ2ZX.js → chunk-BCKEP5S6.js} +2 -2
  10. package/dist/{chunk-DMPHP7UP.js → chunk-BI2VI6EM.js} +2 -2
  11. package/dist/{chunk-5FCBYK6L.js → chunk-BVEDKMNG.js} +10 -10
  12. package/dist/{chunk-OCNSIBMP.js → chunk-C3IQQKXL.js} +26 -1
  13. package/dist/{chunk-OCNSIBMP.js.map → chunk-C3IQQKXL.js.map} +1 -1
  14. package/dist/{chunk-PPIZHDJL.js → chunk-CJ2RMTT7.js} +2 -2
  15. package/dist/{chunk-7CJ3DVZW.js → chunk-CWXTNPNZ.js} +2 -2
  16. package/dist/{chunk-TGUESYGF.js → chunk-DFKBTBO3.js} +2 -2
  17. package/dist/{chunk-WHWVYL7B.js → chunk-FSGICFXB.js} +7 -32
  18. package/dist/chunk-FSGICFXB.js.map +1 -0
  19. package/dist/{chunk-3HS6LPFX.js → chunk-FUWFLAWQ.js} +4 -4
  20. package/dist/{chunk-4AIYWWKV.js → chunk-KJRVMMM3.js} +2 -2
  21. package/dist/{chunk-TCF6S7AD.js → chunk-LM2NHRCJ.js} +2 -2
  22. package/dist/{chunk-AEUABPAJ.js → chunk-R5IMFAFR.js} +2 -2
  23. package/dist/{chunk-NTLSMJHW.js → chunk-S7RYFUIK.js} +2 -2
  24. package/dist/{chunk-MLT4QY5X.js → chunk-SLFG7Y76.js} +1 -1
  25. package/dist/chunk-SLFG7Y76.js.map +1 -0
  26. package/dist/{chunk-REJCSA4H.js → chunk-TR3HFH6J.js} +4 -3
  27. package/dist/chunk-TR3HFH6J.js.map +1 -0
  28. package/dist/{chunk-22GKJO5F.js → chunk-VSGMCCGQ.js} +2 -2
  29. package/dist/{chunk-4R3YAQNV.js → chunk-WSJ7RO54.js} +2 -2
  30. package/dist/chunk-XPJQXSC4.js +112 -0
  31. package/dist/chunk-XPJQXSC4.js.map +1 -0
  32. package/dist/chunk-ZCL7C5LH.js +2698 -0
  33. package/dist/chunk-ZCL7C5LH.js.map +1 -0
  34. package/dist/{chunk-ZTPPCHDB.js → chunk-ZQV7UQNE.js} +2 -2
  35. package/dist/cli/bin.js +39 -26
  36. package/dist/cli/bin.js.map +1 -1
  37. package/dist/cli/commands/audit.d.ts +1 -1
  38. package/dist/cli/commands/audit.js +8 -8
  39. package/dist/client-G2WGWPCA.js +11 -0
  40. package/dist/configs/eslint.js +27 -1
  41. package/dist/configs/eslint.js.map +1 -1
  42. package/dist/{convert-to-pnpm-3RVGUBBR.js → convert-to-pnpm-YTFREUJK.js} +6 -6
  43. package/dist/{db-FL7ZBF6H.js → db-XKZYDCKM.js} +9 -9
  44. package/dist/{digest-TZ3QQOSF.js → digest-7UBGUBRK.js} +14 -14
  45. package/dist/{digest-collectors-EHFY2OQC.js → digest-collectors-DJFN5ZDK.js} +8 -8
  46. package/dist/{ensure-site-WIDO6LGU.js → ensure-site-OAAE5YE2.js} +2 -2
  47. package/dist/{forms-notify-target-2LWF5C5B.js → forms-notify-target-UC6PF3AE.js} +2 -2
  48. package/dist/{github-signals-GAHNOAHS.js → github-signals-TX5FPYP6.js} +5 -5
  49. package/dist/{header-image-YN3AQRY3.js → header-image-PSPT7CUM.js} +2 -2
  50. package/dist/{health-endpoint-XLNBWVHK.js → health-endpoint-KVCJYWET.js} +6 -6
  51. package/dist/{health-mirror-EB76EERV.js → health-mirror-XJAKA3EY.js} +3 -3
  52. package/dist/index.d.ts +2 -2
  53. package/dist/index.js +31 -29
  54. package/dist/index.js.map +1 -1
  55. package/dist/{init-YCLSGIEP.js → init-FFE4FCO2.js} +13 -13
  56. package/dist/{launch-FSHDTXSM.js → launch-UD36AHUN.js} +221 -7
  57. package/dist/launch-UD36AHUN.js.map +1 -0
  58. package/dist/match-harness-QRG6XZAR.js +70 -0
  59. package/dist/match-harness-QRG6XZAR.js.map +1 -0
  60. package/dist/migrate-Y7R4CESL.js +7 -0
  61. package/dist/{onboard-NKE6GZKL.js → onboard-TM2KESGF.js} +6 -6
  62. package/dist/{orchestrate-MWXENQOS.js → orchestrate-HYVQPRCE.js} +6 -6
  63. package/dist/{pipeline-MATFYQRC.js → pipeline-YROAY6DT.js} +2 -2
  64. package/dist/{preflight-4VQP7JJL.js → preflight-RYIP54NY.js} +7 -7
  65. package/dist/{prismic-ci-DM352L2Q.js → prismic-ci-6GKKZGB5.js} +13 -11
  66. package/dist/prismic-ci-6GKKZGB5.js.map +1 -0
  67. package/dist/{prismic-models-Q32ARSYS.js → prismic-models-P6FWWVRS.js} +5 -5
  68. package/dist/prospect/types.d.ts +1 -1
  69. package/dist/{prospect-audit-MT6RGMOY.js → prospect-audit-PRHU53HS.js} +5 -5
  70. package/dist/{prospect-audits-RVYEA4KH.js → prospect-audits-R3LWF6U7.js} +10 -4
  71. package/dist/recipes/sync-configs.d.ts +1 -1
  72. package/dist/recipes/sync-configs.js +4 -4
  73. package/dist/{render-JE5P26H5.js → render-YGNGTX3W.js} +2 -2
  74. package/dist/render-from-row-IFSBHWDH.js +1 -1
  75. package/dist/{report-6FFHGUUS.js → report-LQS4ECTM.js} +12 -12
  76. package/dist/{report-mirror-IP64R7B3.js → report-mirror-HDTOJE64.js} +3 -3
  77. package/dist/{schema-2V7BGYVT.js → schema-A4WCPIF4.js} +1 -1
  78. package/dist/{schema-2V7BGYVT.js.map → schema-A4WCPIF4.js.map} +1 -1
  79. package/dist/{self-updating-5DVDIEHB.js → self-updating-MWXIZLL7.js} +6 -6
  80. package/dist/{selftest-LJYXBIO5.js → selftest-VXTHRPRN.js} +9 -9
  81. package/dist/{site-mirror-UKHREAD6.js → site-mirror-WON4FTJ2.js} +3 -3
  82. package/dist/{smoke-suite-NUTC7IUK.js → smoke-suite-7DGITCTZ.js} +6 -6
  83. package/dist/{submissions-5GKJPCIH.js → submissions-OVZPVBYG.js} +2 -2
  84. package/dist/{svelte-codemods-U4NIT6CV.js → svelte-codemods-WSV2FGNG.js} +6 -6
  85. package/dist/{sync-configs-S2JYY6QU.js → sync-configs-RRIHNLIJ.js} +7 -7
  86. package/dist/{types-OIQZ5hWN.d.ts → types-D_bpy89i.d.ts} +1 -1
  87. package/dist/{upgrade-YFZCMUPE.js → upgrade-VTD6V4HS.js} +6 -6
  88. package/dist/util/git.d.ts +46 -1
  89. package/dist/util/git.js +7 -1
  90. package/package.json +1 -1
  91. package/dist/chunk-7MKT4I5T.js +0 -62
  92. package/dist/chunk-7MKT4I5T.js.map +0 -1
  93. package/dist/chunk-MLT4QY5X.js.map +0 -1
  94. package/dist/chunk-REJCSA4H.js.map +0 -1
  95. package/dist/chunk-WHWVYL7B.js.map +0 -1
  96. package/dist/chunk-XTK5VIQB.js.map +0 -1
  97. package/dist/client-M2V6FHH5.js +0 -11
  98. package/dist/launch-FSHDTXSM.js.map +0 -1
  99. package/dist/migrate-K4JETR36.js +0 -7
  100. package/dist/prismic-ci-DM352L2Q.js.map +0 -1
  101. /package/dist/{announce-PGT4SZZB.js.map → announce-CLQYX5I4.js.map} +0 -0
  102. /package/dist/{blux-NXQLMS4W.js.map → blux-4KNXUP2K.js.map} +0 -0
  103. /package/dist/{bump-deps-6OR7LL4E.js.map → bump-deps-C5G2EFBB.js.map} +0 -0
  104. /package/dist/{chunk-LS6QPANO.js.map → chunk-2LYOAB2E.js.map} +0 -0
  105. /package/dist/{chunk-KQB2BX4V.js.map → chunk-2ZAZ7ETT.js.map} +0 -0
  106. /package/dist/{chunk-B6NUZ2ZX.js.map → chunk-BCKEP5S6.js.map} +0 -0
  107. /package/dist/{chunk-DMPHP7UP.js.map → chunk-BI2VI6EM.js.map} +0 -0
  108. /package/dist/{chunk-5FCBYK6L.js.map → chunk-BVEDKMNG.js.map} +0 -0
  109. /package/dist/{chunk-PPIZHDJL.js.map → chunk-CJ2RMTT7.js.map} +0 -0
  110. /package/dist/{chunk-7CJ3DVZW.js.map → chunk-CWXTNPNZ.js.map} +0 -0
  111. /package/dist/{chunk-TGUESYGF.js.map → chunk-DFKBTBO3.js.map} +0 -0
  112. /package/dist/{chunk-3HS6LPFX.js.map → chunk-FUWFLAWQ.js.map} +0 -0
  113. /package/dist/{chunk-4AIYWWKV.js.map → chunk-KJRVMMM3.js.map} +0 -0
  114. /package/dist/{chunk-TCF6S7AD.js.map → chunk-LM2NHRCJ.js.map} +0 -0
  115. /package/dist/{chunk-AEUABPAJ.js.map → chunk-R5IMFAFR.js.map} +0 -0
  116. /package/dist/{chunk-NTLSMJHW.js.map → chunk-S7RYFUIK.js.map} +0 -0
  117. /package/dist/{chunk-22GKJO5F.js.map → chunk-VSGMCCGQ.js.map} +0 -0
  118. /package/dist/{chunk-4R3YAQNV.js.map → chunk-WSJ7RO54.js.map} +0 -0
  119. /package/dist/{chunk-ZTPPCHDB.js.map → chunk-ZQV7UQNE.js.map} +0 -0
  120. /package/dist/{client-M2V6FHH5.js.map → client-G2WGWPCA.js.map} +0 -0
  121. /package/dist/{convert-to-pnpm-3RVGUBBR.js.map → convert-to-pnpm-YTFREUJK.js.map} +0 -0
  122. /package/dist/{db-FL7ZBF6H.js.map → db-XKZYDCKM.js.map} +0 -0
  123. /package/dist/{digest-TZ3QQOSF.js.map → digest-7UBGUBRK.js.map} +0 -0
  124. /package/dist/{digest-collectors-EHFY2OQC.js.map → digest-collectors-DJFN5ZDK.js.map} +0 -0
  125. /package/dist/{ensure-site-WIDO6LGU.js.map → ensure-site-OAAE5YE2.js.map} +0 -0
  126. /package/dist/{forms-notify-target-2LWF5C5B.js.map → forms-notify-target-UC6PF3AE.js.map} +0 -0
  127. /package/dist/{github-signals-GAHNOAHS.js.map → github-signals-TX5FPYP6.js.map} +0 -0
  128. /package/dist/{header-image-YN3AQRY3.js.map → header-image-PSPT7CUM.js.map} +0 -0
  129. /package/dist/{health-endpoint-XLNBWVHK.js.map → health-endpoint-KVCJYWET.js.map} +0 -0
  130. /package/dist/{health-mirror-EB76EERV.js.map → health-mirror-XJAKA3EY.js.map} +0 -0
  131. /package/dist/{init-YCLSGIEP.js.map → init-FFE4FCO2.js.map} +0 -0
  132. /package/dist/{migrate-K4JETR36.js.map → migrate-Y7R4CESL.js.map} +0 -0
  133. /package/dist/{onboard-NKE6GZKL.js.map → onboard-TM2KESGF.js.map} +0 -0
  134. /package/dist/{orchestrate-MWXENQOS.js.map → orchestrate-HYVQPRCE.js.map} +0 -0
  135. /package/dist/{pipeline-MATFYQRC.js.map → pipeline-YROAY6DT.js.map} +0 -0
  136. /package/dist/{preflight-4VQP7JJL.js.map → preflight-RYIP54NY.js.map} +0 -0
  137. /package/dist/{prismic-models-Q32ARSYS.js.map → prismic-models-P6FWWVRS.js.map} +0 -0
  138. /package/dist/{prospect-audit-MT6RGMOY.js.map → prospect-audit-PRHU53HS.js.map} +0 -0
  139. /package/dist/{prospect-audits-RVYEA4KH.js.map → prospect-audits-R3LWF6U7.js.map} +0 -0
  140. /package/dist/{render-JE5P26H5.js.map → render-YGNGTX3W.js.map} +0 -0
  141. /package/dist/{report-6FFHGUUS.js.map → report-LQS4ECTM.js.map} +0 -0
  142. /package/dist/{report-mirror-IP64R7B3.js.map → report-mirror-HDTOJE64.js.map} +0 -0
  143. /package/dist/{self-updating-5DVDIEHB.js.map → self-updating-MWXIZLL7.js.map} +0 -0
  144. /package/dist/{selftest-LJYXBIO5.js.map → selftest-VXTHRPRN.js.map} +0 -0
  145. /package/dist/{site-mirror-UKHREAD6.js.map → site-mirror-WON4FTJ2.js.map} +0 -0
  146. /package/dist/{smoke-suite-NUTC7IUK.js.map → smoke-suite-7DGITCTZ.js.map} +0 -0
  147. /package/dist/{submissions-5GKJPCIH.js.map → submissions-OVZPVBYG.js.map} +0 -0
  148. /package/dist/{svelte-codemods-U4NIT6CV.js.map → svelte-codemods-WSV2FGNG.js.map} +0 -0
  149. /package/dist/{sync-configs-S2JYY6QU.js.map → sync-configs-RRIHNLIJ.js.map} +0 -0
  150. /package/dist/{upgrade-YFZCMUPE.js.map → upgrade-VTD6V4HS.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runMigrations
3
- } from "./chunk-OCNSIBMP.js";
3
+ } from "./chunk-C3IQQKXL.js";
4
4
  import {
5
5
  defaultCredentialsPath
6
6
  } from "./chunk-6VAL7XJT.js";
@@ -48,4 +48,4 @@ export {
48
48
  readDbConfig,
49
49
  openDb
50
50
  };
51
- //# sourceMappingURL=chunk-22GKJO5F.js.map
51
+ //# sourceMappingURL=chunk-VSGMCCGQ.js.map
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-R53VL3RI.js";
5
5
  import {
6
6
  withRecipe
7
- } from "./chunk-ZTPPCHDB.js";
7
+ } from "./chunk-ZQV7UQNE.js";
8
8
  import {
9
9
  defaultSpawn
10
10
  } from "./chunk-LBYOLBW7.js";
@@ -142,4 +142,4 @@ async function healthEndpoint(site, deps = { spawn: defaultSpawn }) {
142
142
  export {
143
143
  healthEndpoint
144
144
  };
145
- //# sourceMappingURL=chunk-4R3YAQNV.js.map
145
+ //# sourceMappingURL=chunk-WSJ7RO54.js.map
@@ -0,0 +1,112 @@
1
+ // src/db/prospect-audits.ts
2
+ import { randomBytes } from "crypto";
3
+ function generateToken() {
4
+ return randomBytes(16).toString("base64url");
5
+ }
6
+ function isValidToken(s) {
7
+ return /^[A-Za-z0-9_-]{22}$/.test(s);
8
+ }
9
+ function newProspectAuditId() {
10
+ return `pa_${crypto.randomUUID()}`;
11
+ }
12
+ function siteKey(url) {
13
+ const raw = url.trim();
14
+ if (raw === "") return "";
15
+ try {
16
+ const u = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`);
17
+ const host = u.hostname.toLowerCase().replace(/^www\./, "");
18
+ const path = u.pathname.replace(/\/+$/, "");
19
+ return `${host}${path}`;
20
+ } catch {
21
+ return raw;
22
+ }
23
+ }
24
+ async function createProspectAudit(db, audit) {
25
+ const id = newProspectAuditId();
26
+ const token = generateToken();
27
+ await db.insertInto("prospect_audits").values({
28
+ id,
29
+ token,
30
+ url: audit.url,
31
+ site_key: siteKey(audit.url),
32
+ business: audit.business,
33
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
34
+ status: audit.status ?? "complete",
35
+ result_json: audit.resultJson
36
+ }).execute();
37
+ return { id, token };
38
+ }
39
+ async function getProspectAuditByToken(db, token) {
40
+ const row = await db.selectFrom("prospect_audits").select([
41
+ "id",
42
+ "url",
43
+ "business",
44
+ "created_at",
45
+ "status",
46
+ "result_json",
47
+ "overrides_json",
48
+ "edited_at",
49
+ "opened_at"
50
+ ]).where("token", "=", token).executeTakeFirst();
51
+ return row ?? null;
52
+ }
53
+ var MAX_RECENT_PROSPECT_AUDITS = 100;
54
+ function clampLimit(limit) {
55
+ const n = Number.isFinite(limit) ? Math.trunc(limit) : 1;
56
+ return Math.min(Math.max(n, 1), MAX_RECENT_PROSPECT_AUDITS);
57
+ }
58
+ async function listRecentProspectAudits(db, limit) {
59
+ return db.selectFrom("prospect_audits").select(["id", "token", "url", "business", "status", "created_at", "edited_at", "opened_at"]).orderBy("created_at", "desc").limit(clampLimit(limit)).execute();
60
+ }
61
+ var OVERRIDES_MAX_LEN = 512e3;
62
+ function buildOverrideMap(v) {
63
+ if (v === null || typeof v !== "object") return null;
64
+ const proto = Object.getPrototypeOf(v);
65
+ if (proto !== Object.prototype && proto !== null) return null;
66
+ if (Object.hasOwn(v, "__proto__")) return null;
67
+ const built = {};
68
+ for (const [key, entry] of Object.entries(v)) {
69
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null;
70
+ const { original, text } = entry;
71
+ if (typeof original !== "string" || typeof text !== "string") return null;
72
+ built[key] = { original, text };
73
+ }
74
+ return built;
75
+ }
76
+ function serialiseOverrides(overrides) {
77
+ let json;
78
+ try {
79
+ const built = buildOverrideMap(overrides);
80
+ if (built === null) return null;
81
+ json = JSON.stringify(built);
82
+ } catch {
83
+ return null;
84
+ }
85
+ return json.length > OVERRIDES_MAX_LEN ? null : json;
86
+ }
87
+ async function setProspectAuditOverrides(db, token, overrides) {
88
+ const serialised = serialiseOverrides(overrides);
89
+ if (serialised === null) return { status: "invalid", token };
90
+ const existing = await getProspectAuditByToken(db, token);
91
+ if (!existing) return { status: "not-found", token };
92
+ await db.updateTable("prospect_audits").set({ overrides_json: serialised, edited_at: (/* @__PURE__ */ new Date()).toISOString() }).where("token", "=", token).execute();
93
+ return { status: "updated", token };
94
+ }
95
+ async function touchProspectAuditOpened(db, token) {
96
+ await db.updateTable("prospect_audits").set({ opened_at: (/* @__PURE__ */ new Date()).toISOString() }).where("token", "=", token).execute();
97
+ }
98
+
99
+ export {
100
+ generateToken,
101
+ isValidToken,
102
+ newProspectAuditId,
103
+ siteKey,
104
+ createProspectAudit,
105
+ getProspectAuditByToken,
106
+ MAX_RECENT_PROSPECT_AUDITS,
107
+ listRecentProspectAudits,
108
+ OVERRIDES_MAX_LEN,
109
+ setProspectAuditOverrides,
110
+ touchProspectAuditOpened
111
+ };
112
+ //# sourceMappingURL=chunk-XPJQXSC4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db/prospect-audits.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport type { Db } from \"./client.js\";\n\n/** 16 random bytes → 22-char base64url string: unguessable, URL-safe. */\nexport function generateToken(): string {\n return randomBytes(16).toString(\"base64url\");\n}\n\n/** Anything not exactly generateToken()-shaped is a probe, not a report. */\nexport function isValidToken(s: string): boolean {\n return /^[A-Za-z0-9_-]{22}$/.test(s);\n}\n\n/** Opaque, collision-free id (mirrors newSubmissionId / newDeadLetterId).\n * crypto is a Node 20 global — no new dep. Nothing reads this format; `token`\n * is the public handle. */\nexport function newProspectAuditId(): string {\n return `pa_${crypto.randomUUID()}`;\n}\n\n/** \"complete\" when every pipeline stage succeeded; \"partial\" when any stage\n * failed or was deliberately skipped (StageResult's `ok: false` covers\n * both — the pipeline doesn't distinguish them at this layer). The report\n * itself already degrades each failed section to \"not measured\"; this is\n * what lets a future dashboard tell complete from partial without\n * deserializing the whole result_json. */\nexport type ProspectAuditStatus = \"complete\" | \"partial\";\n\n/**\n * The lineage handle for a site: one key for every way of writing its address.\n *\n * `url` is kept exactly as it was given, because what we audited is a fact\n * about the run. It is the wrong thing to group by, though, and the corpus\n * already proves it — `beachfrontdentistry.com` and `beachfrontdentistry.com/`\n * are two separate histories in the table today, as are the two spellings of\n * ludlowkingsley.com. Nothing has broken only because nothing yet compares two\n * audits of one site. The moment the report offers a before and an after, a\n * trailing slash turns a returning client into a stranger.\n *\n * Scheme, a leading \"www.\", a trailing slash, a query string and a fragment all\n * go: none of them distinguish one site from another. The path stays, because a\n * site living in a subdirectory genuinely is a different site.\n *\n * Never throws. A key we cannot compute must not take an audit down with it —\n * an unparseable input falls back to its trimmed self, which groups nothing and\n * so is no worse than having no key at all.\n */\nexport function siteKey(url: string): string {\n const raw = url.trim();\n if (raw === \"\") return \"\";\n try {\n const u = new URL(/^[a-z][a-z0-9+.-]*:\\/\\//i.test(raw) ? raw : `https://${raw}`);\n const host = u.hostname.toLowerCase().replace(/^www\\./, \"\");\n const path = u.pathname.replace(/\\/+$/, \"\");\n return `${host}${path}`;\n } catch {\n return raw;\n }\n}\n\nexport type NewProspectAudit = {\n url: string;\n business: string | null;\n resultJson: string;\n /** Defaults to \"complete\" — the column's own SQL default — for callers\n * (tests, ad-hoc scripts) that don't track per-stage outcomes. The\n * prospect-audit CLI, the only real writer, always computes and passes\n * this explicitly. */\n status?: ProspectAuditStatus;\n};\n\nexport async function createProspectAudit(\n db: Db,\n audit: NewProspectAudit,\n): Promise<{ id: string; token: string }> {\n const id = newProspectAuditId();\n const token = generateToken();\n await db\n .insertInto(\"prospect_audits\")\n .values({\n id,\n token,\n url: audit.url,\n site_key: siteKey(audit.url),\n business: audit.business,\n created_at: new Date().toISOString(),\n status: audit.status ?? \"complete\",\n result_json: audit.resultJson,\n })\n .execute();\n return { id, token };\n}\n\nexport type ProspectAuditRow = {\n id: string;\n url: string;\n business: string | null;\n created_at: string;\n status: string;\n result_json: string;\n /** The operator's edits as stored JSON, or null when this report has never\n * been edited. Deliberately NOT parsed here: the JSON route will place this\n * string into its response body as-is, exactly as it already does with\n * `result_json`, so the row hands back what was validated on the way in. */\n overrides_json: string | null;\n edited_at: string | null;\n opened_at: string | null;\n};\n\nexport async function getProspectAuditByToken(\n db: Db,\n token: string,\n): Promise<ProspectAuditRow | null> {\n const row = await db\n .selectFrom(\"prospect_audits\")\n .select([\n \"id\",\n \"url\",\n \"business\",\n \"created_at\",\n \"status\",\n \"result_json\",\n \"overrides_json\",\n \"edited_at\",\n \"opened_at\",\n ])\n .where(\"token\", \"=\", token)\n .executeTakeFirst();\n return row ?? null;\n}\n\n/** The columns the cockpit's /audits listing page shows. Deliberately NOT\n * `result_json` — large, and useless to a list — but `token` IS selected:\n * the listing needs it to build each row's `/r/{token}` link, so it's\n * rendered only into an href, never displayed as a raw value. */\nexport type ProspectAuditListItem = {\n id: string;\n token: string;\n url: string;\n business: string | null;\n status: string;\n created_at: string;\n /** Whether an operator has edited this report, and when someone last opened\n * it — both cheap TEXT columns, and the two facts the listing needs to show\n * an \"edited\" marker without reading `overrides_json`. */\n edited_at: string | null;\n opened_at: string | null;\n};\n\n/** Ceiling on `listRecentProspectAudits`' `limit`, enforced defensively (a\n * caller passing 10_000 must not get 10_000 rows) — well past anything a\n * listing page would ever render on one screen. */\nexport const MAX_RECENT_PROSPECT_AUDITS = 100;\n\n/** Clamp a caller-supplied limit into [1, MAX_RECENT_PROSPECT_AUDITS], with a\n * safe fallback of 1 for anything non-finite (NaN, ±Infinity) rather than\n * letting a bad input reach the query unclamped. */\nfunction clampLimit(limit: number): number {\n const n = Number.isFinite(limit) ? Math.trunc(limit) : 1;\n return Math.min(Math.max(n, 1), MAX_RECENT_PROSPECT_AUDITS);\n}\n\n/** Newest-first audits for the cockpit's /audits listing page, capped both\n * defensively (see clampLimit) and by an index (migration 0010) so the\n * ORDER BY never falls back to a raw scan + a temp b-tree sort — see the\n * EXPLAIN-query-plan gate, tests/db/query-plans.test.ts. */\nexport async function listRecentProspectAudits(\n db: Db,\n limit: number,\n): Promise<ProspectAuditListItem[]> {\n return db\n .selectFrom(\"prospect_audits\")\n .select([\"id\", \"token\", \"url\", \"business\", \"status\", \"created_at\", \"edited_at\", \"opened_at\"])\n .orderBy(\"created_at\", \"desc\")\n .limit(clampLimit(limit))\n .execute();\n}\n\n/** One replaced string and the generated text it replaced. */\nexport type Override = { original: string; text: string };\nexport type OverrideMap = Record<string, Override>;\n\n/**\n * Ceiling on one report's override map, measured on the SERIALISED JSON rather\n * than on the entry count — one entry holding a megabyte is the case that\n * matters, and the string is what actually gets stored and re-served.\n *\n * The arithmetic, not a round number: the report carries roughly 227 editable\n * strings (the check battery alone, before fixes, claims and goal copy). An\n * operator who replaced every one of them, at around 300 characters of\n * `original` plus 300 of `text` each, writes on the order of 136 KB. This is\n * about four times that worst realistic case, which leaves the honest editor\n * unbounded in practice while still bounding a hand-crafted POST.\n *\n * The bound matters more here than it does for the sibling `setReportCommentary`\n * (2 000 characters): commentary is read when a report is composed, whereas this\n * value is served back inline on every fetch of the report, out of a metered\n * store. 2 288 891 characters were accepted in a single write before this cap.\n */\nexport const OVERRIDES_MAX_LEN = 512_000;\n\nexport type SetOverridesResult =\n | { status: \"updated\"; token: string }\n | { status: \"invalid\"; token: string }\n | { status: \"not-found\"; token: string };\n\n/**\n * The map that will actually be stored — a fresh `{ original, text }` pair per\n * entry, built out of the validated strings themselves — or null for anything\n * that is not a flat map of `{ original, text }` string pairs.\n *\n * Validating and BUILDING are one step deliberately, and this function returns\n * the map rather than a type guard for that reason. Every defect this code has\n * had was the same shape: one object was checked and a different one was handed\n * to `JSON.stringify`. `stringify` honours a `toJSON` method, so the entry\n * `{ original: \"a\", text: \"b\", toJSON: () => 5 }` validated as a pair and stored\n * `{\"k\":5}` — bytes this function never saw, in the one place that claims to be\n * the only gate they can be caught at. Reading each field once and keeping what\n * was read closes the gap by construction: what is stored IS what was approved,\n * and a getter that would answer differently on a second read has no second\n * read. Key order comes straight from `Object.entries`, which is the order\n * `JSON.stringify` would have used, so a round trip is stable and diffs stay\n * legible.\n *\n * EXTRA KEYS ON AN ENTRY ARE DROPPED, and that is a POLICY, not something\n * building the map here forces. A third design satisfies both halves at once:\n * build from the validated fields AND reject any entry carrying a key other than\n * `original` and `text`. Under it the `toJSON` gap, the double-read gap and the\n * extra keys all close by construction exactly as they do now, and a cycle or a\n * BigInt in a third key comes back `invalid` — because the third key is itself\n * the rejection reason, not because `JSON.stringify` threw on it. Dropping and\n * rejecting are therefore independent of building; the choice is DROP, and this\n * is the argument for it rather than a consequence of the rewrite.\n *\n * The consumer reads `original` and `text` and nothing else — the website\n * matches `original` against the generated string before it substitutes `text`.\n * An entry key nobody reads is dead weight re-served on every fetch of the\n * report out of a metered store, which is the same cost `OVERRIDES_MAX_LEN`\n * exists to bound; so the answer to a third key is to stop storing it, not to\n * refuse the operator's words over it. Dropping also keeps the cheap\n * forward-compatible case working: an editor that starts sending a field this\n * schema has not learned yet still saves what was typed.\n *\n * The line that drop stops at is the one this file already draws: it is the\n * ENTRY's own keys that are dropped. An unknown TOP-LEVEL key is a whole\n * override — operator content — and is validated like any other, never silently\n * discarded. The `__proto__` rule below is that same distinction reaching the\n * opposite verdict, for the same reason.\n *\n * The drop IS silent, and that is the cost being accepted: a map is stored that\n * is not byte-identical to the one sent, and the caller is told `updated` all\n * the same. It is acceptable only because what differs is always fields nobody\n * reads — it would not be acceptable for anything a reader could miss.\n *\n * This runs BEFORE the read, the same order `setReportCommentary` uses, so a\n * malformed body costs no round trip.\n *\n * It is also the ONLY place a malformed value can be caught. The JSON route\n * never parses what it serves — today it returns `result_json` untouched, and\n * once it carries overrides too it will place this string into the body the\n * same way, for the reason its own comment gives: parsing and re-serialising\n * there would add a failure mode between the database and the consumer for no\n * gain. Nothing downstream will notice a non-JSON value here; it would simply\n * break every subsequent fetch of that report. So: validate on the way in,\n * once, and never store anything this function has not approved.\n *\n * \"Only place\" is about MALFORMEDNESS, and is not a claim that no other limit is\n * needed. `OVERRIDES_MAX_LEN` bounds the string that gets STORED, never the\n * request body: a POST carrying megabytes of junk in keys nobody reads is parsed\n * in full and walked in full, and then stores 33 bytes and answers `updated`.\n * Bounding the body is the HTTP route's job, with a body limit there — not a\n * second check here.\n *\n * Both null guards are load-bearing, because `typeof null === \"object\"`: the\n * outer one keeps `Object.entries(null)` from throwing, and the inner one keeps\n * a null ENTRY from being read for `.original` — the exact input that crashed\n * the website's own override layer during its review.\n *\n * The PROTOTYPE check is the one that stops this function passing vacuously.\n * `Object.entries()` on a Map, a Set or a Date returns `[]`, so an entry walk\n * never runs its body and approved all three: a Map stored `{}` and silently\n * discarded every override in it, and a Date stored a bare JSON string where a\n * map belongs — each reporting \"updated\" and stamping `edited_at`.\n * Arrays fall out of the same check (`Array.prototype !== Object.prototype`), so\n * there is no separate `Array.isArray` guard on the outer value.\n *\n * `Object.create(null)` IS accepted: a null-prototype object serialises exactly\n * like `{}`, which is the only property this validator's consumer cares about,\n * and refusing it would reject a legitimate map for a reason invisible in JSON.\n *\n * A CROSS-REALM plain object is rejected, though, because its prototype is the\n * OTHER realm's `Object.prototype` and an identity comparison sees a stranger.\n * Nothing produces one today — a route handler's `JSON.parse` builds its objects\n * in this realm — and rejecting is the safe direction, but it would be a mystery\n * to a future worker-thread or `vm` consumer handed a map across the boundary.\n *\n * A `__proto__` KEY is rejected rather than stripped. `JSON.parse` makes it an\n * ordinary own property, and stored verbatim a consumer doing\n * `Object.assign({}, parsed)` does get its prototype replaced — the consumer\n * here is the website, a separate codebase, and this function's whole premise is\n * that nothing downstream will check.\n *\n * It is rejected rather than stripped for the distinction above, not for any\n * general rule against silent drops — such a rule would condemn the entry-key\n * drop in the same breath. `__proto__` at the TOP level is a whole override, so\n * stripping it would discard operator content and still report `updated`, on a\n * report that quietly lost an edit. An entry key is read by nobody, so dropping\n * one loses nothing. One rule, applied consistently, with opposite outcomes for\n * a stated reason.\n */\nfunction buildOverrideMap(v: unknown): OverrideMap | null {\n if (v === null || typeof v !== \"object\") return null;\n const proto = Object.getPrototypeOf(v) as unknown;\n if (proto !== Object.prototype && proto !== null) return null;\n if (Object.hasOwn(v, \"__proto__\")) return null;\n const built: OverrideMap = {};\n for (const [key, entry] of Object.entries(v as Record<string, unknown>)) {\n if (entry === null || typeof entry !== \"object\" || Array.isArray(entry)) return null;\n // One read each, and the values that were read are the values that are kept.\n const { original, text } = entry as Partial<Override>;\n if (typeof original !== \"string\" || typeof text !== \"string\") return null;\n built[key] = { original, text };\n }\n return built;\n}\n\n/**\n * The exact string that will be stored, or null when the input is malformed.\n *\n * Building and serialising are one step, inside one try, because READING the\n * caller's object can throw on an input that is otherwise well-formed. There are\n * three such paths through `buildOverrideMap`, and the try covers all of them:\n *\n * 1. `Object.entries(v)` — an enumerable getter on the map itself, or a\n * Proxy's `ownKeys` / `getOwnPropertyDescriptor` trap.\n * 2. `const { original, text } = entry` — a getter on the ENTRY, which throws\n * after the map has been walked and before either field is validated.\n * 3. `built[key] = { original, text }` — `built` starts as `{}`, so the\n * assignment walks `Object.prototype`, and a setter poisoned there under\n * that key name throws on the WRITE rather than on any read.\n *\n * `Object.getPrototypeOf(v)` and `Object.hasOwn(v, \"__proto__\")` are two more,\n * both trappable on a Proxy and both ahead of the walk.\n *\n * `setProspectAuditOverrides` is typed to return one of three statuses, and an\n * escaping TypeError is a fourth outcome its callers have no branch for. A value\n * that cannot be read is malformed input, not an exceptional condition — and the\n * caller already has a branch for malformed input.\n *\n * `JSON.stringify` itself can no longer throw here: it is handed a plain object\n * of plain `{ original, text }` string pairs, which has no cycle, no BigInt and\n * no `toJSON` to honour. That used to be the other half of this try — a circular\n * reference or a BigInt in a valid-looking entry threw — and both are now simply\n * dropped with the rest of the entry's extra keys. The try stays wrapped around\n * the whole thing anyway: the day a field that is not a string is added, this\n * still returns `invalid` rather than throwing past the return type.\n *\n * The size cap is measured HERE, on the CONSTRUCTED string: that is the string\n * that gets stored and re-served, an entry count says nothing about it, and the\n * caller's object may be arbitrarily larger than the pairs taken out of it.\n * Which is also what it is NOT: a request-body limit. An oversized body is\n * parsed and walked in full before 33 bytes are stored and `updated` returned,\n * so whoever writes the HTTP route still owes it a body limit of its own.\n */\nfunction serialiseOverrides(overrides: OverrideMap): string | null {\n let json: string;\n try {\n const built = buildOverrideMap(overrides);\n if (built === null) return null;\n json = JSON.stringify(built);\n } catch {\n return null;\n }\n return json.length > OVERRIDES_MAX_LEN ? null : json;\n}\n\n/**\n * Replace one report's override map wholesale and stamp `edited_at`.\n *\n * Whole-map rather than per-key: the caller always holds the complete map, and\n * a partial write has no way to express a deletion. An empty map is legitimate\n * and clears every override — an editor that can set but not unset traps the\n * operator in whatever they first typed.\n *\n * What is stored is REBUILT from the validated strings, not the object passed\n * in, so an entry keeps its `original` and its `text` and nothing else: any\n * other key on it is dropped rather than written through. See\n * `buildOverrideMap` for why that is the contract.\n *\n * LAST WRITE WINS, and two writers silently clobber each other: the second\n * write replaces the first's whole map, with nothing to tell either of them it\n * happened. That is accepted rather than overlooked — there is one operator\n * team, and an editor is open for minutes, so the window is small and the cost\n * of losing it is a paragraph retyped. If a second editor ever exists, the\n * column an optimistic check would key on is `edited_at`: read it with the\n * report, send it back with the write, refuse the write if it moved.\n *\n * `result_json` stays untouched. There is still no way to write it.\n */\nexport async function setProspectAuditOverrides(\n db: Db,\n token: string,\n overrides: OverrideMap,\n): Promise<SetOverridesResult> {\n const serialised = serialiseOverrides(overrides);\n if (serialised === null) return { status: \"invalid\", token };\n\n const existing = await getProspectAuditByToken(db, token);\n if (!existing) return { status: \"not-found\", token };\n\n await db\n .updateTable(\"prospect_audits\")\n .set({ overrides_json: serialised, edited_at: new Date().toISOString() })\n .where(\"token\", \"=\", token)\n .execute();\n return { status: \"updated\", token };\n}\n\n/**\n * Record that a report was opened by someone who is not editing it.\n *\n * Best effort by contract: the caller must not let a failure here fail the\n * response. Knowing when a prospect last looked is useful; it is not worth\n * turning a read route into one that can 500.\n */\nexport async function touchProspectAuditOpened(db: Db, token: string): Promise<void> {\n await db\n .updateTable(\"prospect_audits\")\n .set({ opened_at: new Date().toISOString() })\n .where(\"token\", \"=\", token)\n .execute();\n}\n"],"mappings":"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","names":[]}