failproofai 1.0.4-beta.7 → 1.0.4

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 (221) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +5 -5
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
  8. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  13. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  16. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  24. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
  27. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  35. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +4 -37
  36. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  37. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/index.html +1 -1
  40. package/.next/standalone/.next/server/app/index.rsc +14 -14
  41. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  42. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  43. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  44. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  45. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  46. package/.next/standalone/.next/server/app/page.js +2 -2
  47. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  50. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  51. package/.next/standalone/.next/server/app/policies/page.js +4 -4
  52. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
  57. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -4
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  66. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  67. package/.next/standalone/.next/server/app/projects/page.js +3 -3
  68. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  71. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -15
  72. package/.next/standalone/.next/server/app/settings/page.js +4 -4
  73. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{[externals]__0v1hz15._.js → [externals]__0-9ti7z._.js} +1 -1
  76. package/.next/standalone/.next/server/chunks/{[externals]__1nb206a._.js → [externals]__0u3wrvq._.js} +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/{_0otft92._.js → _1-akg6w._.js} +2 -2
  85. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +10 -10
  86. package/.next/standalone/.next/server/chunks/_1vslkoe._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1fvfw59._.js → [root-of-the-server]__00-s7h8._.js} +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
  94. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__12hbpha._.js → [root-of-the-server]__09flfsx._.js} +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
  97. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0kokc8z._.js → [root-of-the-server]__0n_lxhg._.js} +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
  101. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0dxf928._.js → [root-of-the-server]__190won8._.js} +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n2txhk._.js → [root-of-the-server]__1ctpynv._.js} +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
  104. package/.next/standalone/.next/server/chunks/ssr/_02yoeej._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/ssr/{_0a0zlpr._.js → _04rbnaq._.js} +2 -2
  106. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/{_0s-77v5._.js → _0h_douw._.js} +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/_1_ozgif._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +14 -51
  111. package/.next/standalone/.next/server/chunks/ssr/{_07t3jol._.js → _1u8-lu2._.js} +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +3 -0
  113. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  115. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +69 -1
  116. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  119. package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  122. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  123. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  124. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  125. package/.next/standalone/.next/server/pages/404.html +1 -1
  126. package/.next/standalone/.next/server/pages/500.html +1 -1
  127. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  128. package/.next/standalone/.next/server/server-reference-manifest.json +21 -65
  129. package/.next/standalone/.next/static/chunks/{0f1pznnc7zlub.js → 0-tyrpwuc3lla.js} +1 -1
  130. package/.next/standalone/.next/static/chunks/0ca7octhod0g2.js +69 -0
  131. package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +1 -0
  132. package/.next/standalone/.next/static/chunks/{1csubx9gwhsf6.js → 1j6o9min_6qa9.js} +1 -1
  133. package/.next/standalone/.next/static/chunks/27cl8h-c2w7cd.js +1 -0
  134. package/.next/standalone/.next/static/chunks/{0jpd8dv2vk930.css → 29-iqnp9v_axt.css} +1 -1
  135. package/.next/standalone/.next/static/chunks/{3uyhmm01g64k4.css → 2k9f4tyv04809.css} +0 -1
  136. package/.next/standalone/.next/static/chunks/{0t6ea7d3bkmdv.js → 2lwipmv5etc8m.js} +1 -1
  137. package/.next/standalone/.next/static/chunks/2pd16tn5qmx13.js +1 -0
  138. package/.next/standalone/.next/static/chunks/{0ntdfr2552x-h.js → 2u8vh73tpw28_.js} +1 -1
  139. package/.next/standalone/.next/static/chunks/{15uon66hpwbgg.js → 31u94yukw9fz-.js} +1 -1
  140. package/.next/standalone/.next/static/chunks/{2pwfrn6_mw-1u.js → 3p9_8x5ezce-9.js} +1 -1
  141. package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +1 -0
  142. package/.next/standalone/.next/static/chunks/{083fk73jtv9a0.js → 3tjji451hrgc0.js} +1 -1
  143. package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +1 -0
  144. package/.next/standalone/.next/static/chunks/{turbopack-3s3c-u_u_l0u9.js → turbopack-3p73obe0xmrxm.js} +1 -1
  145. package/.next/standalone/.opencode/plugins/failproofai.mjs +27 -51
  146. package/.next/standalone/app/actions/get-scheduled-audit.ts +0 -5
  147. package/.next/standalone/app/actions/update-scheduled-audit.ts +57 -18
  148. package/.next/standalone/app/api/auth/status/route.ts +2 -3
  149. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +34 -140
  150. package/.next/standalone/app/audit/_components/audit-poster.tsx +10 -29
  151. package/.next/standalone/app/audit/_components/come-back-better-section.tsx +1 -6
  152. package/.next/standalone/app/audit/_components/empty-state.tsx +1 -4
  153. package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +5 -19
  154. package/.next/standalone/app/audit/_components/share-templates.ts +44 -110
  155. package/.next/standalone/app/audit/audit-styles.css +1 -132
  156. package/.next/standalone/app/settings/page.tsx +1 -1
  157. package/.next/standalone/app/settings/settings-client.tsx +45 -50
  158. package/.next/standalone/lib/auth/api-server-client.ts +5 -33
  159. package/.next/standalone/lib/claude-sessions.ts +27 -101
  160. package/.next/standalone/lib/codex-sessions.ts +4 -20
  161. package/.next/standalone/lib/copilot-sessions.ts +1 -32
  162. package/.next/standalone/lib/cursor-sessions.ts +5 -62
  163. package/.next/standalone/lib/sqlite-reader.ts +1 -78
  164. package/.next/standalone/package.json +9 -9
  165. package/.next/standalone/server.js +1 -1
  166. package/bin/failproofai.mjs +1 -1
  167. package/dist/cli.mjs +500 -1762
  168. package/dist/worker.mjs +16 -138
  169. package/lib/auth/api-server-client.ts +5 -33
  170. package/lib/claude-sessions.ts +27 -101
  171. package/lib/codex-sessions.ts +4 -20
  172. package/lib/copilot-sessions.ts +1 -32
  173. package/lib/cursor-sessions.ts +5 -62
  174. package/lib/sqlite-reader.ts +1 -78
  175. package/package.json +9 -9
  176. package/pi-extension/index.ts +5 -50
  177. package/src/audit/cache.ts +0 -6
  178. package/src/audit/cli-adapters/claude.ts +0 -13
  179. package/src/audit/cli-login.ts +1 -4
  180. package/src/audit/cli.ts +7 -157
  181. package/src/audit/harm-report.ts +4 -111
  182. package/src/audit/index.ts +15 -286
  183. package/src/audit/machine-store.ts +9 -8
  184. package/src/audit/redact-example.ts +15 -324
  185. package/src/audit/report-harm.ts +37 -33
  186. package/src/audit/report.ts +3 -6
  187. package/src/audit/schedule-cli.ts +33 -54
  188. package/src/audit/scoring.ts +0 -49
  189. package/src/audit/types.ts +0 -52
  190. package/src/hooks/builtin-policies.ts +2 -67
  191. package/src/hooks/configure-wizard.ts +0 -55
  192. package/src/hooks/fp-config.ts +48 -83
  193. package/src/hooks/fp-home.ts +0 -27
  194. package/src/hooks/integrations.ts +14 -51
  195. package/src/hooks/uninstall-cli.ts +2 -17
  196. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-4ja6q._.js +0 -4
  197. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cr3xu._.js +0 -4
  198. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04px77e._.js +0 -4
  199. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0egr5s1._.js +0 -6
  200. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0i0l1di._.js +0 -4
  201. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0r3c1_0._.js +0 -4
  202. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17xnxlx._.js +0 -4
  203. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ckpmkk._.js +0 -4
  204. package/.next/standalone/.next/server/chunks/ssr/_04ng_6x._.js +0 -3
  205. package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +0 -3
  206. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +0 -3
  207. package/.next/standalone/.next/static/chunks/0divvxng208xo.js +0 -1
  208. package/.next/standalone/.next/static/chunks/27du9n7ssja7l.js +0 -1
  209. package/.next/standalone/.next/static/chunks/2h_pcb1y8_jde.js +0 -1
  210. package/.next/standalone/app/actions/get-leaks.ts +0 -104
  211. package/.next/standalone/app/audit/_components/leak-section.tsx +0 -192
  212. package/src/audit/desktop-notify.ts +0 -420
  213. package/src/audit/leak-fingerprint.ts +0 -198
  214. package/src/audit/leak-record.ts +0 -248
  215. package/src/audit/leak-scan.ts +0 -268
  216. package/src/audit/leak-store.ts +0 -220
  217. package/src/audit/macos-notifier.ts +0 -298
  218. package/src/hooks/notice.ts +0 -24
  219. /package/.next/standalone/.next/static/{YiPsVpjrc-pEQoN7XvhZ0 → dg8VQ7Ejlo0Qsh_x92oIt}/_buildManifest.js +0 -0
  220. /package/.next/standalone/.next/static/{YiPsVpjrc-pEQoN7XvhZ0 → dg8VQ7Ejlo0Qsh_x92oIt}/_clientMiddlewareManifest.js +0 -0
  221. /package/.next/standalone/.next/static/{YiPsVpjrc-pEQoN7XvhZ0 → dg8VQ7Ejlo0Qsh_x92oIt}/_ssgManifest.js +0 -0
@@ -68,9 +68,13 @@ const dim = (s: string) => wrap(ANSI_DIM, s);
68
68
  const bold = (s: string) => wrap(ANSI_BOLD, s);
69
69
 
70
70
  /**
71
- * Turn scheduled audits on. Email is optional: `--email` signs in, while a
72
- * plain `--schedule` enables the local scan and desktop notification without
73
- * prompting for an account.
71
+ * Turn scheduled audits on, signing in first if needed.
72
+ *
73
+ * Scheduling and mailing are ONE decision — the reason to put a scan on a timer
74
+ * is to be told what it found — so this requires a session, exactly as the
75
+ * dashboard's `setAutoAuditAction` does. A timer set with nobody to tell is a
76
+ * switch that reads as on and produces nothing, discoverable only by noticing
77
+ * that no digest ever arrives.
74
78
  *
75
79
  * The interval is written and then RE-READ, so what is printed is what the
76
80
  * config actually kept — `readIntervalDays` owns the 1..90 clamp and a second
@@ -104,13 +108,17 @@ export async function runScheduleOn(
104
108
  if (bad) throw new ScheduleCliError(bad);
105
109
  }
106
110
 
107
- const signedIn = emailArg === undefined
108
- ? { user: readAuth()?.user ?? null, prompted: false }
109
- : await ensureSignedIn(emailArg);
111
+ const { user, prompted } = await ensureSignedIn(emailArg);
110
112
 
111
113
  const next = updateConfig({
114
+ // Stamped in the SAME call that sets `auto`, never separately: this records
115
+ // that a person completed a sign-in and read the disclosure printed below,
116
+ // and it is what `reportHarm` gates sending on. A machine that inherited
117
+ // `auto` from a release where it meant "scan locally" has no stamp and
118
+ // sends nothing until it comes through here.
112
119
  audit: {
113
120
  auto: true,
121
+ reportsConsentedAt: Date.now(),
114
122
  ...(days !== undefined ? { intervalDays: days } : {}),
115
123
  },
116
124
  });
@@ -126,14 +134,12 @@ export async function runScheduleOn(
126
134
  // The list is the real payload from `report-harm.ts`: machine id, hostname,
127
135
  // platform, the window bounds and the redacted examples.
128
136
  const summary = [
129
- `every ${interval} day${interval === 1 ? "" : "s"} · desktop notification on leaks`,
130
- signedIn.user
131
- ? `masked leak alerts also go to ${signedIn.user.email}`
132
- : "email is optional · add it with --email you@example.com",
133
- "email contains only the most recent masked credential exposure",
137
+ `every ${interval} day${interval === 1 ? "" : "s"} · reports to ${user.email}`,
138
+ "you only hear from it when a scan finds something harmful",
139
+ "each report sends: finding counts, redacted example commands, this machine's name",
134
140
  ];
135
141
 
136
- if (signedIn.prompted) {
142
+ if (prompted) {
137
143
  // A sign-in just drew the frame, so the result continues it and the `└`
138
144
  // closes both at once — rather than the frame ending and a loose line
139
145
  // appearing underneath.
@@ -152,7 +158,7 @@ export async function runScheduleOn(
152
158
  // exists to make visible.
153
159
  warnIfDaemonWontRun();
154
160
 
155
- if (signedIn.prompted) {
161
+ if (prompted) {
156
162
  outro("failproofai audit --status · when the next scan is due");
157
163
  } else {
158
164
  process.stdout.write(dim(`\n failproofai audit --status when the next scan is due\n\n`));
@@ -211,22 +217,24 @@ export function runScheduleStatus(): void {
211
217
 
212
218
  // A session whose refresh window has closed cannot mint another access token,
213
219
  // so it is a destination in name only. Showing the address for one would tell
214
- // somebody their alerts are going somewhere they are not.
220
+ // somebody their digests are going somewhere they are not.
215
221
  const live = auth && auth.refresh_expires_at * 1000 > Date.now() ? auth : null;
216
- // Beside the schedule, because they are the two halves of "what does this
217
- // machine do on its own" — one decides whether it scans, the other whether it
218
- // is allowed to interrupt you about what it found.
219
- detail.push([
220
- "leak notifications",
221
- config.audit.notify ? green("on") : dim("off (failproofai audit --notify)"),
222
- ]);
223
-
224
222
  detail.push(["reports to", live ? live.user.email : dim("— signed out")]);
225
223
  if (on && !live) {
226
224
  // The state the reporter surfaces as "signed-out". Named here for the same
227
- // reason the settings panel names it: the scans and desktop notifications
228
- // keep running, so silence about email would look like the feature failing.
229
- detail.push(["", pink("scans and desktop alerts continue; email alerts are paused")]);
225
+ // reason the settings panel names it: the scans keep running, so silence
226
+ // about the digests would look like the feature failing.
227
+ detail.push(["", pink("scans continue; digests are paused until you sign in")]);
228
+ } else if (on && config.audit.reportsConsentedAt === undefined) {
229
+ // Signed in, scheduled, and still not sending: this machine set `audit.auto`
230
+ // when it only meant "scan locally", so nothing has consented to the digest
231
+ // leaving the box. Without this row the status screen would show a healthy
232
+ // schedule and a live address and still mail nothing, with no explanation
233
+ // anywhere the user can see.
234
+ detail.push([
235
+ "",
236
+ pink("scans continue; digests need a fresh opt-in — run `--schedule` to turn them on"),
237
+ ]);
230
238
  }
231
239
 
232
240
  detail.push(["daemon", describeDaemon(daemon)]);
@@ -338,32 +346,3 @@ export function asScheduleError(err: unknown): never {
338
346
  }
339
347
  throw err;
340
348
  }
341
-
342
- /**
343
- * Turn the desktop banner on or off.
344
- *
345
- * A separate command from `--schedule` rather than a flag on it, because they
346
- * are separate decisions with different lifetimes: somebody who wants the
347
- * weekly scan and not the banner should not have to re-state their schedule to
348
- * say so, and somebody silencing the banner in an annoyed moment should not
349
- * discover they also turned off the scan.
350
- *
351
- * Writes the same `audit.notify` key the dashboard toggle writes and the audit
352
- * child reads, so the two surfaces cannot disagree.
353
- */
354
- export function runNotifyToggle(enable: boolean): void {
355
- try {
356
- updateConfig({ audit: { notify: enable } });
357
- } catch (err) {
358
- process.stderr.write(
359
- `Could not update the setting: ${err instanceof Error ? err.message : String(err)}\n`,
360
- );
361
- process.exit(1);
362
- }
363
- process.stdout.write(
364
- enable
365
- ? `\n${green("on")} leak notifications — this machine will tell you when a scan finds a credential.\n\n`
366
- : `\n${dim("off")} leak notifications — scans continue, and findings still appear in ` +
367
- `\`failproofai audit\` and the emailed digest.\n\n`,
368
- );
369
- }
@@ -1,55 +1,6 @@
1
1
  /**
2
2
  * Score derivation for the audit dashboard.
3
3
  *
4
- * ## SWITCHED OFF — nothing in the product calls any of this (2026-09-07)
5
- *
6
- * Every function below is intact and still covered by
7
- * `__tests__/audit/scoring.test.ts`, deliberately: the score is commented out at
8
- * its call sites, not deleted, so restoring it is un-commenting rather than
9
- * rewriting. The call sites, all commented and cross-referenced back to here:
10
- * `audit-dashboard.tsx` (the four `useMemo`s, the poster/how-to-improve/
11
- * come-back-better props, and the `score`/`grade` PostHog properties),
12
- * `audit-poster.tsx` (the `.poster-score` block, the PNG filename, the share
13
- * payload), `how-to-improve-section.tsx` (the projected sub-heading),
14
- * `share-templates.ts` (19 of 20 templates interpolated the number).
15
- *
16
- * ### Why
17
- *
18
- * Two reasons, one product and one correctness.
19
- *
20
- * The product reason is the owner's: the audit tried to do too many things
21
- * shallowly, and grading a machine 0-100 with a letter tier was not the half
22
- * worth keeping.
23
- *
24
- * The correctness reason is worse and is the one to read before switching it
25
- * back on. **`deriveScore` never reads `enabledInConfig`** — only
26
- * `projectedScore` does, at the filter below. So the number the dashboard
27
- * rendered as "enable all N → projected {score}" was undeliverable by
28
- * construction: a user who took the prescription, enabled the policies and
29
- * re-ran got the SAME score with the projection gone. Advertised up to +22,
30
- * delivered 0. There is also no time window, so a hit denied today is counted
31
- * against the machine forever.
32
- *
33
- * Two further things measured at the same time, neither of them the score's
34
- * fault but both worth knowing if this comes back:
35
- *
36
- * - `penaltyFor` consumes `row.hits`, i.e. OCCURRENCES. Occurrences amplify
37
- * against distinct values by 10.7x-41x and non-uniformly by class, so the
38
- * sum does not preserve rank order between classes. Fixing that needs a
39
- * per-distinct-value carrier, which `AuditCount` does not have.
40
- * - `syntheticRank`, `scoreToPercentile` and `COHORT_SIZE` had already lost
41
- * their last caller before this change — the cohort they describe does not
42
- * exist. They are dead either way.
43
- *
44
- * ### To restore
45
- *
46
- * Un-comment the call sites listed above (each carries its original inline) and
47
- * put the score-bearing share copy back from the block at the bottom of
48
- * `share-templates.ts`. Make `score`/`grade` required again in `ShareCtx` and in
49
- * `audit-poster.tsx`'s `Props`, and the compiler will walk you through the rest.
50
- * Fix the `enabledInConfig` defect first, or it will be advertising the same
51
- * undeliverable number.
52
- *
53
4
  * Score is on 0-100, mapped to letter grades that anchor the leaderboard
54
5
  * + tier prose. The thresholds match the reference design (assets/audit):
55
6
  *
@@ -45,9 +45,6 @@ export interface TranscriptMetadata {
45
45
  mtimeMs: number;
46
46
  /** Byte size of the transcript file. Used for cache invalidation. */
47
47
  sizeBytes: number;
48
- /** Optional host-authored session purpose. Used only to distinguish
49
- * deliberate credential-scanner research from evidence of a leak. */
50
- sessionDescription?: string;
51
48
  }
52
49
 
53
50
  /** Per-session detector state. Detectors mutate this freely. */
@@ -122,27 +119,6 @@ export interface AuditCount {
122
119
  }
123
120
 
124
121
  /** Per-transcript scan result (also the per-transcript cache value). */
125
- /** One credential sighting, already fingerprinted. */
126
- export interface TranscriptLeak {
127
- /** Salted, machine-local id for the DISTINCT value. */
128
- id: string;
129
- /** Masked rendering + class + length. Never the value. */
130
- fingerprint: import("./leak-fingerprint").LeakFingerprint;
131
- /** Identifier it was assigned to, when it came from an assignment. */
132
- name: string | null;
133
- /** Rule that found it. */
134
- rule: string;
135
- /** Whether a vendor shape matched — gates whether an alert may name a console. */
136
- shaped: boolean;
137
- timestamp: string;
138
- cwd: string;
139
- toolName: string;
140
- /** Sent by the agent, or received by it. Different exposures, different fixes. */
141
- direction: "input" | "result";
142
- /** Home-shortened path the value was read from or written to, when there was one. */
143
- path: string | null;
144
- }
145
-
146
122
  export interface TranscriptAuditResult {
147
123
  /** Cache key: matches TranscriptMetadata.transcriptPath. */
148
124
  transcriptPath: string;
@@ -164,20 +140,6 @@ export interface TranscriptAuditResult {
164
140
  hitsByName: Record<string, number>;
165
141
  /** Up to 3 example commands per policy/detector (later coalesced upstream). */
166
142
  examplesByName: Record<string, { timestamp: string; cwd: string; example: string }[]>;
167
- /**
168
- * Credentials found in this transcript, one entry per SIGHTING.
169
- *
170
- * Deliberately separate from `hitsByName`, which counts by RULE and cannot
171
- * enumerate distinct values. These carry the fingerprint rather than the
172
- * credential — the raw value never leaves `scanOneTranscript`.
173
- *
174
- * Optional so a cached result written before this field existed still parses;
175
- * absent reads as "this transcript predates leak scanning", not "clean".
176
- */
177
- leaks?: TranscriptLeak[];
178
- /** The session was explicitly generating/researching credential fixtures,
179
- * so its matches are examples rather than evidence of user leakage. */
180
- leakScanSuppressed?: "credential-research";
181
143
  /** First/last timestamp per name. */
182
144
  rangeByName: Record<string, { first: string; last: string }>;
183
145
  }
@@ -217,20 +179,6 @@ export interface AuditResult {
217
179
  * Lets the dashboard answer "is this policy already on?" for
218
180
  * detector-mapped policies that may not have hit during this audit. */
219
181
  enabledBuiltinNames: string[];
220
- /** Ids of credentials encountered by this scan, including ones already in
221
- * the persistent leak record. Scheduled desktop notifications key on this
222
- * list so every completed scan that still sees a leak can notify; the
223
- * narrower `newLeakIds` remains for first-seen-only surfaces. */
224
- leakIds?: string[];
225
- /**
226
- * Ids of credentials seen for the FIRST time in this scan.
227
- *
228
- * What a notice keys on. Not "findings this scan saw" — a key already in the
229
- * record must not re-alert however many fresh sightings it picks up, and one
230
- * seen for the first time must alert even though its rule has fired before.
231
- * Optional so a cached result predating leak scanning still parses.
232
- */
233
- newLeakIds?: string[];
234
182
  }
235
183
 
236
184
  /** CLI-supplied options for `runAudit()`. Set by `bin/failproofai.mjs`. */
@@ -126,80 +126,15 @@ const JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
126
126
 
127
127
  // sanitizeApiKeys
128
128
  const API_KEY_PATTERNS: Array<[RegExp, string]> = [
129
- // ── Anthropic ── prefix doc-verified: platform.claude.com says a key
130
- // "starts with `sk-ant-`" and its own example is `sk-ant-api03-...`.
131
- [/sk-ant-api03-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
132
- // Minted by the Claude CLI's OAuth flow and billed against a Pro/Max
133
- // subscription rather than the API account — a different thing to revoke.
134
- [/sk-ant-oat01-[A-Za-z0-9\-_]{20,}/, "Anthropic OAuth token"],
135
129
  [/sk-ant-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
136
-
137
- // ── OpenAI ── the body charset EXCLUDES `-` and `_` on purpose: with them,
138
- // this rule's run would bridge the hyphen in `sk-proj-` and report every
139
- // project key a second time under the wrong label.
140
130
  [/sk-proj-[A-Za-z0-9\-_]{20,}/, "OpenAI project API key"],
141
- [/(?<!\w)sk-svcacct-[A-Za-z0-9\-_]{20,}/, "OpenAI service-account key"],
142
- // Highest blast radius in the family: it authenticates the organisation
143
- // admin surface, and lives at a DIFFERENT console from the other keys.
144
- [/(?<!\w)sk-admin-[A-Za-z0-9\-_]{20,}/, "OpenAI admin key"],
145
-
146
- // ── OpenAI-compatible gateways ──
147
- [/sk-or-v1-[A-Za-z0-9\-_]{20,}/, "OpenRouter API key"],
148
- // A BARE `sk-` CANNOT BE ATTRIBUTED, and calling it OpenAI is what this rule
149
- // used to do. LiteLLM's own docs say its virtual keys "must start with sk-",
150
- // and DeepSeek plus every other OpenAI-compatible proxy mints the same shape
151
- // — so on a machine running a gateway (this one does) the old label sent the
152
- // user to the wrong console to revoke a key that was never OpenAI's. The
153
- // honest label names the family, not a vendor.
154
- [/sk-[A-Za-z0-9]{20,}/, "OpenAI-compatible key (issuer unknown)"],
155
-
156
- // ── GitHub ── all five documented types. github.blog's token-format post
157
- // names ghp/gho/ghu/ghs/ghr; only two of them shipped before.
158
- [/github_pat_[A-Za-z0-9_]{82}/, "GitHub fine-grained token"],
131
+ [/sk-[A-Za-z0-9]{20,}/, "OpenAI API key"],
159
132
  [/ghp_[A-Za-z0-9]{36}/, "GitHub personal access token"],
160
- [/gho_[A-Za-z0-9]{36}/, "GitHub OAuth token"],
161
- [/ghu_[A-Za-z0-9]{36}/, "GitHub user-to-server token"],
162
- [/ghs_[A-Za-z0-9]{36}/, "GitHub server-to-server token"],
163
- [/ghr_[A-Za-z0-9]{36}/, "GitHub refresh token"],
164
- [/glpat-[A-Za-z0-9\-_]{20,}/, "GitLab personal access token"],
165
-
166
- // ── AWS ── the ID is the PUBLIC half; the paired 40-char secret is the
167
- // credential. Reported anyway because an exposed ID names the account and is
168
- // what a user searches their console for. `ASIA` (temporary STS) was missing.
133
+ [/github_pat_[A-Za-z0-9_]{82}/, "GitHub fine-grained token"],
169
134
  [/AKIA[A-Z0-9]{16}/, "AWS access key ID"],
170
- [/ASIA[A-Z0-9]{16}/, "AWS temporary access key ID"],
171
-
172
- // ── Payments ── Stripe documents `pk_*` as "Safe to expose: Yes", so only
173
- // the secret and restricted halves appear here.
174
135
  [/sk_live_[A-Za-z0-9]{24,}/, "Stripe live secret key"],
175
136
  [/sk_test_[A-Za-z0-9]{24,}/, "Stripe test secret key"],
176
- [/rk_live_[A-Za-z0-9]{24,}/, "Stripe restricted key"],
177
-
178
- // ── Google ── `AIza` also covers Firebase Web and Maps browser keys, which
179
- // are public by design, so the label stays non-committal about severity.
180
137
  [/AIza[0-9A-Za-z\-_]{35}/, "Google API key"],
181
-
182
- // ── Messaging ── the Telegram bot token was matched by ZERO of the shipped
183
- // patterns and was found only because a phone-number regex tripped over it;
184
- // two live-looking ones sit in this machine's corpus right now. Slack's
185
- // prefixes were in the Rust collector but not here — one of nine such
186
- // divergences between the two engines.
187
- [/\d{8,10}:[A-Za-z0-9_-]{35}/, "Telegram bot token"],
188
- [/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token"],
189
- [/xapp-[0-9]-[A-Za-z0-9-]{10,}/, "Slack app-level token"],
190
-
191
- // ── Model hosts ──
192
- [/hf_[A-Za-z0-9]{30,}/, "Hugging Face token"],
193
- [/gsk_[A-Za-z0-9]{40,}/, "Groq API key"],
194
- [/r8_[A-Za-z0-9]{30,}/, "Replicate token"],
195
- [/xai-[A-Za-z0-9]{40,}/, "xAI API key"],
196
-
197
- // ── Registries ──
198
- [/npm_[A-Za-z0-9]{36}/, "npm access token"],
199
- [/dop_v1_[a-f0-9]{64}/, "DigitalOcean token"],
200
- [/shpat_[a-fA-F0-9]{32}/, "Shopify access token"],
201
- [/sb_secret_[A-Za-z0-9\-_]{20,}/, "Supabase secret key"],
202
- [/sbp_[a-f0-9]{40}/, "Supabase personal token"],
203
138
  ];
204
139
 
205
140
  // sanitizeConnectionStrings
@@ -74,7 +74,6 @@ import {
74
74
  probeDaemonEndToEnd,
75
75
  uninstallDaemonService,
76
76
  } from "./daemon-service";
77
- import { installMacNotifier } from "../audit/macos-notifier";
78
77
  import { hookLogWarn } from "./hook-logger";
79
78
  import {
80
79
  readCloudCredentials,
@@ -106,7 +105,6 @@ import {
106
105
  type RetryProbe,
107
106
  } from "./onboarding-attempt";
108
107
  import { acquireOnboardingLock } from "./onboarding-lock";
109
- import { readConfigRaw, updateConfig } from "./fp-config";
110
108
 
111
109
  export interface WizardIO {
112
110
  stdin?: TTYIn;
@@ -756,19 +754,6 @@ export async function runConfigureWizard(
756
754
  // question, and no flag can supply it.
757
755
  const preAnswered = Boolean(answers.token);
758
756
 
759
- // Capture the preference BEFORE the daemon setup writes config.json. A fresh
760
- // machine reaches `setDaemonConfigured(true)` with no config file; that helper
761
- // necessarily materialises the conservative no-file default (`auto: false`).
762
- // Looking after that write cannot distinguish "the user switched it off"
763
- // from "setup just wrote its fallback", which is how new installs ended up
764
- // with a seven-day scheduler that was explicitly disabled.
765
- const initialAudit = readConfigRaw().raw.audit;
766
- const hadExplicitAuditAuto =
767
- !!initialAudit &&
768
- typeof initialAudit === "object" &&
769
- !Array.isArray(initialAudit) &&
770
- typeof (initialAudit as Record<string, unknown>).auto === "boolean";
771
-
772
757
 
773
758
  // Running the wizard itself under sudo configures the WRONG ACCOUNT, and
774
759
  // does it silently: homedir() becomes /root, so the hooks land in root's
@@ -1453,50 +1438,10 @@ export async function runConfigureWizard(
1453
1438
  // binary filename are both derived from it.
1454
1439
  if (daemonInstalled) {
1455
1440
  setDaemonConfigured(true, cliVersion);
1456
- // A completed setup is the default opt-in to the LOCAL seven-day scan. It
1457
- // does not imply an email address or make a network request: reporting is
1458
- // independently gated by the signed-in audit identity. Reconfiguration
1459
- // preserves an explicit `auto: false`, so the user's off switch remains an
1460
- // off switch instead of being undone whenever they repair or upgrade the
1461
- // daemon.
1462
- if (!hadExplicitAuditAuto) {
1463
- try {
1464
- updateConfig({ audit: { auto: true } });
1465
- } catch {
1466
- // The daemon and hooks are already installed. Keep setup successful;
1467
- // the status screen will expose that scheduling could not be persisted.
1468
- }
1469
- }
1470
1441
  // Only now — the unit points at the new binary, so older ones are no
1471
1442
  // longer referenced by anything. Keeps the previous version for an
1472
1443
  // offline rollback.
1473
1444
  pruneOldDaemonBinaries();
1474
-
1475
- // The delivery path for what the scheduled audit finds. macOS only, and a
1476
- // no-op everywhere else: a Linux box's audit talks to the session bus
1477
- // itself, while on a Mac the daemon runs outside the GUI session and cannot
1478
- // reach Notification Center at all — see `audit/macos-notifier.ts`.
1479
- //
1480
- // Installed here, with the daemon, because it exists only to deliver what
1481
- // the daemon's scheduled scan produces, and removed alongside it by
1482
- // `uninstall`. Silent, and not a step in the wizard: it needs no password,
1483
- // asks nothing, and narrating it would be an apology for the delivery half
1484
- // of a feature the user just switched on. What they are actually asked —
1485
- // does this machine scan, and may it interrupt — are real settings, and
1486
- // both are theirs to change afterwards.
1487
- //
1488
- // Its failure is not the wizard's failure. A Mac where `osacompile` is
1489
- // missing or launchd refuses the job must still finish installing its
1490
- // daemon, its hooks and its policies, which are the parts that enforce
1491
- // anything; the notice in-CLI still reaches them either way.
1492
- const notifier = installMacNotifier();
1493
- if (process.platform === "darwin" && !notifier.installed) {
1494
- hookLogWarn(`macOS notification helper was not installed: ${notifier.reason ?? "unknown error"}`);
1495
- stdout.write(
1496
- "\nSystem notifications could not be installed. Scheduled scans and email alerts\n" +
1497
- "still work; re-run `failproofai config` after checking the local LaunchAgent.\n\n",
1498
- );
1499
- }
1500
1445
  }
1501
1446
 
1502
1447
  // Telemetry runs concurrently with the install (never rejects, 5s-bounded) so
@@ -308,65 +308,49 @@ export interface FpConfig {
308
308
  };
309
309
  audit: {
310
310
  /**
311
- * Scan this machine on a schedule. Delivery is deliberately separate:
312
- * desktop notifications are controlled by `notify`, while a verified local
313
- * email identity adds email alerts without changing the timer.
311
+ * Scan this machine on a schedule, and mail a digest when a scan finds
312
+ * something harmful.
314
313
  *
315
- * A signed-out machine with this on is therefore the normal local-only
316
- * state rather than a contradiction: it keeps scanning, keeps its local
317
- * dashboard current, and raises desktop notifications for credential leaks.
318
- * A refresh token expiring must not silently switch off a background
319
- * security feature.
314
+ * ONE switch, not two. An earlier revision split this into `auto` (scan
315
+ * locally, no account) and `email_enabled` (mail me, sign in), and the two
316
+ * could only ever disagree a machine scanning on a timer with nothing to
317
+ * report it to is a feature that looks on and does nothing visible. The
318
+ * reason to put a scan on a timer is to be TOLD, so scheduling and mailing
319
+ * are the same decision and take the same switch.
320
320
  *
321
- * ON for a configured machine that has never stated an opinion, OFF for
322
- * every machine we could not read an opinion from which is not the same
323
- * default twice, and the difference is the whole design.
321
+ * A signed-out machine with this on is therefore a real, expected state
322
+ * rather than a contradiction: it keeps scanning and keeps its local
323
+ * dashboard current, and the UI says "signed out" until somebody signs back
324
+ * in. Auth gates SETTING it up, never the machine's ongoing work — a
325
+ * refresh token expiring must not silently switch off a background feature
326
+ * somebody configured months ago.
324
327
  *
325
- * The scan reads the CONTENTS of every session transcript on disk, so the
326
- * conservative instinct is to keep it off until asked. But the thing it
327
- * looks for now is a leaked credential, and a credential nobody scans for
328
- * is a credential nobody revokes: a switch that is off by default is a
329
- * feature that, for most users, never runs. Setup is where the machine's
330
- * owner is told what it does, so completing setup is the opinion.
331
- *
332
- * Hence the split. Once `config.json` exists AND carries an `audit` table,
333
- * this is on unless it says exactly `false`. Absent file, unparseable JSON,
334
- * or no `audit` table at all still read as OFF, because those are the three
335
- * ways of not knowing, and "we could not tell" must never turn a transcript
336
- * scan on. `crates/failproofaid/src/audit_lane.rs` makes the identical
337
- * distinction over the identical bytes — the daemon and this reader must
338
- * agree, or the settings page describes a machine that is doing something
339
- * else.
340
- *
341
- * Turning it on sends nothing remotely on its own. Email requires a valid
342
- * identity in `audit/session.json`; without one, scans and notifications
343
- * remain fully local.
328
+ * OFF by default, and the asymmetry with `telemetry.enabled` above is
329
+ * deliberate: the audit reads the CONTENTS of every session transcript on
330
+ * disk prompts, file contents, command output so nothing scans on a
331
+ * timer until somebody asks for it.
344
332
  */
345
333
  auto: boolean;
346
334
  /**
347
- * Raise a desktop notification when a scan finds a credential.
348
- *
349
- * Separate from `auto` because they answer different questions: `auto` is
350
- * whether the machine scans, this is whether it interrupts. Somebody who
351
- * wants the scan and not the banner is a coherent person, and without this
352
- * their only way to stop the banner is to stop the scan.
335
+ * When this machine's owner agreed that a scheduled scan may send what it
336
+ * finds off the box, as epoch ms. Absent means they never did.
353
337
  *
354
- * ON by default. Every scheduled scan that still sees a leak may notify,
355
- * which keeps a credential that remains exposed from disappearing after
356
- * its first alert. A silent default would make the common case a user who
357
- * never opens the dashboard and never gave an email a machine that finds
358
- * the key and tells nobody.
338
+ * This does NOT reintroduce the second switch the comment above rejects,
339
+ * and it is never drawn as one. `auto` is what a person sets; this is a
340
+ * record of the disclosure they were shown when they set it. The two cannot
341
+ * drift, because every path that turns `auto` on stamps this in the same
342
+ * call and nothing stamps it alone.
359
343
  *
360
- * On a headless box, container or SSH-only session there may be no desktop
361
- * server to receive the banner. The scheduled scan still completes and
362
- * records its result; notification failure never changes the scan outcome.
363
- */
364
- notify: boolean;
365
- /**
366
- * Legacy migration marker from the earlier harm-digest consent model.
367
- * Retained when reading and writing old configs so upgrades are lossless,
368
- * but it no longer gates scheduling or credential-alert email. A verified
369
- * email identity is now the delivery opt-in.
344
+ * It exists because `auto` CHANGED MEANING. Through 1.0.0 it meant "scan
345
+ * this machine on a timer" and nothing more no account, no network, and
346
+ * the toggle that wrote it said as much in as many words. Harm digests gave
347
+ * the same stored bit a second job: uploading redacted transcript excerpts
348
+ * to the api-server and mailing them. Without a separate record, every
349
+ * machine that opted into the old meaning would have started sending on
350
+ * upgrade, having agreed to nothing of the kind, with the only notice a
351
+ * line in the journal. Gating on the stamp rather than the switch is what
352
+ * keeps that upgrade silent in the safe direction: those machines keep
353
+ * scanning locally and send nothing until somebody opts in again.
370
354
  */
371
355
  reportsConsentedAt?: number;
372
356
  /** Days between scheduled runs. Wall clock, so it survives suspend. */
@@ -418,9 +402,7 @@ export const DEFAULT_CONFIG: FpConfig = {
418
402
  environment: "local",
419
403
  },
420
404
  telemetry: { enabled: true },
421
- // `auto: false` is the no-file case specifically — see the field's doc for
422
- // why a machine that HAS a config reads the other way.
423
- audit: { auto: false, notify: true, intervalDays: DEFAULT_AUDIT_INTERVAL_DAYS },
405
+ audit: { auto: false, intervalDays: DEFAULT_AUDIT_INTERVAL_DAYS },
424
406
  };
425
407
 
426
408
  /**
@@ -513,11 +495,7 @@ export function projectConfig(parsed: Record<string, unknown>): FpConfig {
513
495
  const daemon = (parsed.daemon ?? {}) as Record<string, unknown>;
514
496
  const collector = (parsed.collector ?? {}) as Record<string, unknown>;
515
497
  const telemetry = (parsed.telemetry ?? {}) as Record<string, unknown>;
516
- // Presence, not just contents: `audit.auto` reads differently when the
517
- // table is absent than when it is present-but-silent. See the field's doc.
518
- const auditPresent =
519
- !!parsed.audit && typeof parsed.audit === "object" && !Array.isArray(parsed.audit);
520
- const audit = (auditPresent ? parsed.audit : {}) as Record<string, unknown>;
498
+ const audit = (parsed.audit ?? {}) as Record<string, unknown>;
521
499
  return {
522
500
  // Anything unrecognised reads as `oss`. The failure direction matters: a
523
501
  // corrupt config must not be able to turn cloud reporting ON.
@@ -547,23 +525,15 @@ export function projectConfig(parsed: Record<string, unknown>): FpConfig {
547
525
  // reads as on — the shipped default, and what a config with no
548
526
  // [telemetry] block at all means.
549
527
  telemetry: { enabled: telemetry.enabled !== false },
550
- // A configured machine with no stated opinion scans; a machine we could
551
- // not read an opinion FROM does not. `auditPresent` is what separates
552
- // them, and it is the reason this is not simply `!== false`: with no
553
- // `audit` table there is no opinion to have been stated, only an absence,
554
- // and an absence must not start reading every transcript on disk. Same
555
- // three-way split as `audit_lane.rs`, over the same file.
528
+ // Mirror image of telemetry above: only an explicit `true` switches the
529
+ // scheduled scan on. Absent, misspelled, or `"yes"` all read as off,
530
+ // because the failure direction here is a machine that starts reading
531
+ // every transcript it can find on a timer nobody set.
556
532
  audit: {
557
- auto: auditPresent && audit.auto !== false,
558
- // Default ON, off only on an explicit `false` — a notification the user
559
- // never sees is the same as no detection at all. Ungated on
560
- // `auditPresent`, unlike `auto`: this is read at the moment a
561
- // notification is about to fire, which means a scan already ran and the
562
- // question is only whether to speak.
563
- notify: audit.notify !== false,
533
+ auto: audit.auto === true,
564
534
  intervalDays: readIntervalDays(audit.interval_days),
565
- // Preserve the legacy marker when it is valid; current behavior does
566
- // not consult it.
535
+ // A finite number or nothing. A garbage value reads as absent, which is
536
+ // the direction that sends nothing.
567
537
  reportsConsentedAt:
568
538
  typeof audit.reports_consented_at === "number" &&
569
539
  Number.isFinite(audit.reports_consented_at)
@@ -614,7 +584,6 @@ const OWNED_CONFIG_KEYS: readonly (readonly string[])[] = [
614
584
  ["collector", "sources", "*", "extra_paths"],
615
585
  ["telemetry", "enabled"],
616
586
  ["audit", "auto"],
617
- ["audit", "notify"],
618
587
  ["audit", "interval_days"],
619
588
  ["audit", "reports_consented_at"],
620
589
  ];
@@ -713,20 +682,16 @@ export function writeConfig(config: FpConfig, raw?: Record<string, unknown>): vo
713
682
  ...(config.telemetry.enabled ? {} : { telemetry: { enabled: false } }),
714
683
  // Written ALWAYS, unlike telemetry directly above — the two are opposites
715
684
  // on purpose. Telemetry ships on and is deliberately not advertised;
716
- // the scheduled audit is configured during setup and is meant to be FOUND,
717
- // and a switch
685
+ // the scheduled audit ships off and is meant to be FOUND, and a switch
718
686
  // nobody can see is the same as a switch that does not exist. Emitting both
719
687
  // keys unconditionally also makes "a user's setting survives a rewrite"
720
688
  // total rather than conditional.
721
689
  audit: {
722
690
  auto: config.audit.auto,
723
- // Emitted unconditionally, like `auto`. A switch that only appears in the
724
- // file once you have used it is a switch nobody discovers, and this one
725
- // exists to be found by somebody the banner annoyed.
726
- notify: config.audit.notify,
727
691
  interval_days: config.audit.intervalDays,
728
- // Preserve the legacy consent marker when present. It is not used by the
729
- // current local-schedule + optional-email model.
692
+ // Written only once there IS consent, so an untouched machine's config
693
+ // does not grow a key implying it was asked. It is in
694
+ // `OWNED_CONFIG_KEYS`, so omitting it here really removes it.
730
695
  ...(config.audit.reportsConsentedAt === undefined
731
696
  ? {}
732
697
  : { reports_consented_at: config.audit.reportsConsentedAt }),