failproofai 1.0.4-beta.4 → 1.0.4-beta.6

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 (162) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +5 -5
  32. package/.next/standalone/.next/server/app/audit/page.js +3 -3
  33. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app/index.html +1 -1
  36. package/.next/standalone/.next/server/app/index.rsc +14 -14
  37. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  38. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  40. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  41. package/.next/standalone/.next/server/app/page.js +2 -2
  42. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  45. package/.next/standalone/.next/server/app/policies/page.js +4 -4
  46. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
  50. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -4
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  58. package/.next/standalone/.next/server/app/projects/page.js +3 -3
  59. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  62. package/.next/standalone/.next/server/app/settings/page.js +2 -2
  63. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +4 -4
  76. package/.next/standalone/.next/server/chunks/lib_factory-projects_ts_1eo_bk-._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0-4ja6q._.js} +3 -3
  79. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__00cr3xu._.js} +3 -3
  80. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__04px77e._.js} +3 -3
  81. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__190won8._.js → [root-of-the-server]__0dxf928._.js} +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0s740oi._.js → [root-of-the-server]__0egr5s1._.js} +3 -3
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01wy8d-._.js → [root-of-the-server]__0i0l1di._.js} +3 -3
  84. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n_lxhg._.js → [root-of-the-server]__0kokc8z._.js} +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ctpynv._.js → [root-of-the-server]__0n2txhk._.js} +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__0r3c1_0._.js} +3 -3
  88. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__17xnxlx._.js} +3 -3
  89. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__1ckpmkk._.js} +3 -3
  90. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00-s7h8._.js → [root-of-the-server]__1fvfw59._.js} +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0yl9wb5._.js → [root-of-the-server]__1ui5ur8._.js} +2 -2
  92. package/.next/standalone/.next/server/chunks/ssr/{_0guxnh-._.js → _02b3i98._.js} +2 -2
  93. package/.next/standalone/.next/server/chunks/ssr/_04ng_6x._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/{_1u8-lu2._.js → _07t3jol._.js} +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/{_0h_douw._.js → _0s-77v5._.js} +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +4 -14
  99. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  105. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  106. package/.next/standalone/.next/server/pages/404.html +1 -1
  107. package/.next/standalone/.next/server/pages/500.html +1 -1
  108. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  109. package/.next/standalone/.next/server/server-reference-manifest.json +22 -22
  110. package/.next/standalone/.next/static/chunks/{364cqbpeg1y5i.js → 07c6l1lq5_69b.js} +1 -1
  111. package/.next/standalone/.next/static/chunks/{2bxr2_2h4kaqq.js → 12bsgz8czvcnn.js} +1 -1
  112. package/.next/standalone/.next/static/chunks/{38dqur43qthrl.js → 1gn08w2pgqh09.js} +1 -1
  113. package/.next/standalone/.next/static/chunks/{1i-i0pxg9q_8y.js → 1xgq22tri86an.js} +1 -1
  114. package/.next/standalone/.next/static/chunks/{17tfki9x5rd_7.js → 20xkiqzvfkuvm.js} +1 -1
  115. package/.next/standalone/.next/static/chunks/{42gwu2bsz4ske.js → 2a84ug32tzik2.js} +2 -2
  116. package/.next/standalone/.next/static/chunks/{0po8n4tyotjf2.js → 32m2ryfankf_2.js} +1 -1
  117. package/.next/standalone/.next/static/chunks/3ioub_lpen-sq.js +1 -0
  118. package/.next/standalone/.next/static/chunks/{323cyb3a2s06y.js → 3jxd133cw2vm3.js} +1 -1
  119. package/.next/standalone/.next/static/chunks/{0qx7gav1c2_vu.js → 44ncu88eq6z71.js} +1 -1
  120. package/.next/standalone/app/actions/get-leaks.ts +0 -5
  121. package/.next/standalone/app/actions/update-scheduled-audit.ts +4 -56
  122. package/.next/standalone/app/api/auth/status/route.ts +3 -2
  123. package/.next/standalone/app/settings/page.tsx +1 -1
  124. package/.next/standalone/app/settings/settings-client.tsx +13 -39
  125. package/.next/standalone/lib/auth/api-server-client.ts +6 -6
  126. package/.next/standalone/lib/codex-sessions.ts +20 -4
  127. package/.next/standalone/lib/copilot-sessions.ts +32 -1
  128. package/.next/standalone/lib/cursor-sessions.ts +62 -5
  129. package/.next/standalone/package.json +9 -9
  130. package/.next/standalone/server.js +1 -1
  131. package/dist/cli.mjs +952 -1063
  132. package/dist/worker.mjs +173 -380
  133. package/lib/auth/api-server-client.ts +6 -6
  134. package/lib/codex-sessions.ts +20 -4
  135. package/lib/copilot-sessions.ts +32 -1
  136. package/lib/cursor-sessions.ts +62 -5
  137. package/package.json +9 -9
  138. package/pi-extension/index.ts +10 -18
  139. package/src/audit/cli.ts +72 -81
  140. package/src/audit/harm-report.ts +12 -6
  141. package/src/audit/index.ts +2 -0
  142. package/src/audit/leak-fingerprint.ts +2 -4
  143. package/src/audit/leak-record.ts +13 -9
  144. package/src/audit/leak-scan.ts +1 -1
  145. package/src/audit/leak-store.ts +1 -46
  146. package/src/audit/machine-store.ts +8 -9
  147. package/src/audit/macos-notifier.ts +2 -2
  148. package/src/audit/redact-example.ts +2 -0
  149. package/src/audit/report-harm.ts +31 -46
  150. package/src/audit/schedule-cli.ts +17 -33
  151. package/src/audit/types.ts +5 -0
  152. package/src/hooks/configure-wizard.ts +28 -0
  153. package/src/hooks/fp-config.ts +29 -54
  154. package/src/hooks/handler.ts +1 -79
  155. package/src/hooks/integrations.ts +4 -15
  156. package/src/hooks/notice.ts +12 -179
  157. package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +0 -3
  158. package/.next/standalone/.next/static/chunks/04o5zfl7p5lhx.js +0 -1
  159. package/src/audit/leak-notice.ts +0 -259
  160. /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → aeM365dNTtUZOZvNkX8ba}/_buildManifest.js +0 -0
  161. /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → aeM365dNTtUZOZvNkX8ba}/_clientMiddlewareManifest.js +0 -0
  162. /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → aeM365dNTtUZOZvNkX8ba}/_ssgManifest.js +0 -0
@@ -308,22 +308,15 @@ export interface FpConfig {
308
308
  };
309
309
  audit: {
310
310
  /**
311
- * Scan this machine on a schedule, and mail a digest when a scan finds
312
- * something harmful.
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.
313
314
  *
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
- *
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.
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.
327
320
  *
328
321
  * ON for a configured machine that has never stated an opinion, OFF for
329
322
  * every machine we could not read an opinion from — which is not the same
@@ -345,9 +338,9 @@ export interface FpConfig {
345
338
  * agree, or the settings page describes a machine that is doing something
346
339
  * else.
347
340
  *
348
- * Turning it on sends NOTHING on its own: the digest is gated on
349
- * `reportsConsentedAt` below, not on this switch, so a machine that flips
350
- * on at upgrade scans locally and mails nobody.
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.
351
344
  */
352
345
  auto: boolean;
353
346
  /**
@@ -358,40 +351,22 @@ export interface FpConfig {
358
351
  * wants the scan and not the banner is a coherent person, and without this
359
352
  * their only way to stop the banner is to stop the scan.
360
353
  *
361
- * ON by default, and the direction is the point: this fires at most once
362
- * per distinct credential ever (`leak-notice.ts` claims each finding with
363
- * an O_EXCL marker), so the volume it can reach is bounded by how many
364
- * secrets are actually leaking. A silent default would make the common
365
- * case — a user who never opens the dashboard and never gave an email —
366
- * a machine that finds the key and tells nobody.
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.
367
359
  *
368
- * It does NOT silence the in-CLI notice. That one appears inside a session
369
- * the user is already driving, costs two lines, and is the only channel
370
- * left when there is no desktop to notify — a headless box, a container, an
371
- * SSH session. Turning off every channel at once is not an option this
372
- * offers, because the resulting state is indistinguishable from broken.
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.
373
363
  */
374
364
  notify: boolean;
375
365
  /**
376
- * When this machine's owner agreed that a scheduled scan may send what it
377
- * finds off the box, as epoch ms. Absent means they never did.
378
- *
379
- * This does NOT reintroduce the second switch the comment above rejects,
380
- * and it is never drawn as one. `auto` is what a person sets; this is a
381
- * record of the disclosure they were shown when they set it. The two cannot
382
- * drift, because every path that turns `auto` on stamps this in the same
383
- * call and nothing stamps it alone.
384
- *
385
- * It exists because `auto` CHANGED MEANING. Through 1.0.0 it meant "scan
386
- * this machine on a timer" and nothing more — no account, no network, and
387
- * the toggle that wrote it said as much in as many words. Harm digests gave
388
- * the same stored bit a second job: uploading redacted transcript excerpts
389
- * to the api-server and mailing them. Without a separate record, every
390
- * machine that opted into the old meaning would have started sending on
391
- * upgrade, having agreed to nothing of the kind, with the only notice a
392
- * line in the journal. Gating on the stamp rather than the switch is what
393
- * keeps that upgrade silent in the safe direction: those machines keep
394
- * scanning locally and send nothing until somebody opts in again.
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.
395
370
  */
396
371
  reportsConsentedAt?: number;
397
372
  /** Days between scheduled runs. Wall clock, so it survives suspend. */
@@ -587,8 +562,8 @@ export function projectConfig(parsed: Record<string, unknown>): FpConfig {
587
562
  // question is only whether to speak.
588
563
  notify: audit.notify !== false,
589
564
  intervalDays: readIntervalDays(audit.interval_days),
590
- // A finite number or nothing. A garbage value reads as absent, which is
591
- // the direction that sends nothing.
565
+ // Preserve the legacy marker when it is valid; current behavior does
566
+ // not consult it.
592
567
  reportsConsentedAt:
593
568
  typeof audit.reports_consented_at === "number" &&
594
569
  Number.isFinite(audit.reports_consented_at)
@@ -738,7 +713,8 @@ export function writeConfig(config: FpConfig, raw?: Record<string, unknown>): vo
738
713
  ...(config.telemetry.enabled ? {} : { telemetry: { enabled: false } }),
739
714
  // Written ALWAYS, unlike telemetry directly above — the two are opposites
740
715
  // on purpose. Telemetry ships on and is deliberately not advertised;
741
- // the scheduled audit ships off and is meant to be FOUND, and a switch
716
+ // the scheduled audit is configured during setup and is meant to be FOUND,
717
+ // and a switch
742
718
  // nobody can see is the same as a switch that does not exist. Emitting both
743
719
  // keys unconditionally also makes "a user's setting survives a rewrite"
744
720
  // total rather than conditional.
@@ -749,9 +725,8 @@ export function writeConfig(config: FpConfig, raw?: Record<string, unknown>): vo
749
725
  // exists to be found by somebody the banner annoyed.
750
726
  notify: config.audit.notify,
751
727
  interval_days: config.audit.intervalDays,
752
- // Written only once there IS consent, so an untouched machine's config
753
- // does not grow a key implying it was asked. It is in
754
- // `OWNED_CONFIG_KEYS`, so omitting it here really removes it.
728
+ // Preserve the legacy consent marker when present. It is not used by the
729
+ // current local-schedule + optional-email model.
755
730
  ...(config.audit.reportsConsentedAt === undefined
756
731
  ? {}
757
732
  : { reports_consented_at: config.audit.reportsConsentedAt }),
@@ -35,8 +35,6 @@ import { loadAllCustomHooks } from "./custom-hooks-loader";
35
35
  import type { CustomHook } from "./policy-types";
36
36
  import { persistHookActivity } from "./hook-activity-store";
37
37
  import { deliveryHealth, deliveryHealthLine } from "./delivery-health";
38
- import { shapeNotice, canDeliverNotice, leakNoticeText } from "./notice";
39
- import { sessionNoticePending, markSessionNoticed } from "../audit/leak-notice";
40
38
  import { trackHookEvent, flushHookTelemetry } from "./hook-telemetry";
41
39
  import { resolveCwd } from "./resolve-cwd";
42
40
  import { resolvePermissionMode } from "./resolve-permission-mode";
@@ -106,67 +104,6 @@ export interface HookEventOutcome {
106
104
  stderr: string;
107
105
  }
108
106
 
109
- /**
110
- * Merge a pending leak notice into an outcome, once.
111
- *
112
- * Anchored on Stop wherever the host renders there, and SessionStart only where
113
- * it does not: Stop fires once per turn, AFTER the answer, so it never
114
- * interrupts what the user was reading — and at session open the background
115
- * scan may not have produced the finding yet.
116
- *
117
- * Every failure is swallowed. A courtesy message must never be the reason a
118
- * hook misbehaves, and the finding is on disk either way.
119
- */
120
- function attachLeakNotice(
121
- eventType: string,
122
- cli: IntegrationType | undefined,
123
- outcome: HookEventOutcome,
124
- sessionId: string | undefined,
125
- ): HookEventOutcome {
126
- try {
127
- if (!canDeliverNotice(cli)) return outcome;
128
- // canDeliverNotice() above already proved cli is set; narrow for the call.
129
- if (!cli) return outcome;
130
- const canonical = canonicalizeEventType(eventType, cli);
131
- // STOP ONLY, and the narrowing is the fix for a measured failure rather
132
- // than caution. `notice.ts` derived each host's channel from a live probe
133
- // on **Stop**; SessionStart was allowed here on the assumption that a
134
- // channel proven on one event works on another. It does not. On a real
135
- // machine 499 findings were CLAIMED and the user saw nothing: SessionStart
136
- // fires first in a session, consumed every claim, and the host dropped the
137
- // field — Claude Code documents `hookSpecificOutput.additionalContext` for
138
- // SessionStart, not `systemMessage`.
139
- //
140
- // That is the worst outcome this design has: a finding recorded as
141
- // delivered that reached nobody, and never retried. Stop fires at the end
142
- // of every assistant turn, so nothing is lost by waiting for it — the user
143
- // is reading output at that moment anyway. Add SessionStart back only with
144
- // a probe showing the notice actually rendered there.
145
- if (canonical !== "Stop") return outcome;
146
-
147
- // Keyed on the SESSION and on whether the user has opened the report, not
148
- // on a per-finding "we emitted this once" marker — see
149
- // `sessionNoticePending` for the two ways that marker lost the alert
150
- // permanently. A dropped notice now costs one session's silence instead.
151
- const pending = sessionNoticePending(sessionId ?? "");
152
- if (pending.count === 0) return outcome;
153
-
154
- const shaped = shapeNotice(cli, leakNoticeText(pending.count), outcome.stdout);
155
- if (shaped.stdout === outcome.stdout && !shaped.stderr) return outcome;
156
-
157
- // Claimed only once the notice is actually on the stream, so a crash
158
- // between the two re-notifies rather than silently swallowing the alert.
159
- markSessionNoticed(sessionId ?? "");
160
- return {
161
- exitCode: outcome.exitCode,
162
- stdout: shaped.stdout,
163
- stderr: outcome.stderr + shaped.stderr,
164
- };
165
- } catch {
166
- return outcome;
167
- }
168
- }
169
-
170
107
  export interface EvaluateHookEventOptions {
171
108
  /**
172
109
  * Await pending telemetry POSTs before returning. Defaults to `true`,
@@ -820,22 +757,7 @@ export async function evaluateHookEvent(
820
757
  }
821
758
  }
822
759
 
823
- // Attach a pending leak notice, if this event is one that can carry it.
824
- //
825
- // Emitted HERE rather than in `handleHookEvent`, which is what fixes the
826
- // bug this replaced: `bin/failproofai.mjs`'s `isDaemonConfigured()` branch
827
- // writes `result.stdout`/`result.stderr` and EXITS INSIDE THAT BRANCH — it
828
- // never falls through to `handleHookEvent`. Setup requires the daemon, so
829
- // the previous notice was dead code on every configured machine, and had
830
- // been since it shipped. Both paths return this object, so producing the
831
- // notice here reaches both with no change to either caller.
832
- const withNotice = attachLeakNotice(
833
- eventType,
834
- cli,
835
- { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr },
836
- sessionId,
837
- );
838
- return withNotice;
760
+ return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
839
761
  } finally {
840
762
  if (opts?.awaitTelemetryFlush ?? true) {
841
763
  // Await any un-awaited (`void trackHookEvent(...)`) events fired during
@@ -784,7 +784,6 @@ export const cursor: Integration = {
784
784
  // • exit 2 OR `permissionDecision: "deny"` → `throw new Error(reason)`
785
785
  // (which OpenCode surfaces as a tool-call failure to the agent)
786
786
  // • `additionalContext` → `client.session.prompt(...)` (fire-and-forget)
787
- // • `failproofaiNotice` → `client.tui.showToast(...)` (user-visible)
788
787
  // • everything else → no-op (allow)
789
788
  //
790
789
  // Settings paths:
@@ -957,19 +956,6 @@ async function applyDecision(result, ctx, eventName) {
957
956
  if (out && out.decision && out.decision.behavior === "deny") {
958
957
  throw new Error((out.decision.message) || "Blocked by failproofai");
959
958
  }
960
- const notice = parsed.failproofaiNotice;
961
- if (typeof notice === "string" && notice && ctx && ctx.client && ctx.client.tui) {
962
- try {
963
- await ctx.client.tui.showToast({
964
- body: {
965
- title: "failproofai audit",
966
- message: notice,
967
- variant: "warning",
968
- duration: 12_000,
969
- },
970
- });
971
- } catch { /* a courtesy notice must never break the host hook */ }
972
- }
973
959
  // Forward additional context as a prompt to the session. For Stop /
974
960
  // SubagentStop the prompt is the only force-retry channel (session.idle
975
961
  // already fired), so AWAIT to ensure the SDK round-trip completes before
@@ -1025,7 +1011,10 @@ export default async function failproofaiPlugin({ client, directory }) {
1025
1011
  const claudeEvent = BUS_EVENT_MAP[event.type];
1026
1012
  if (!claudeEvent) return;
1027
1013
  const props = event.properties || {};
1028
- const sessionID = props.sessionID || (props.session && props.session.id) || props.id;
1014
+ const info = props.info || props.session || {};
1015
+
1016
+ const sessionID = info.id || info.sessionID || info.sessionId || info.session_id ||
1017
+ props.sessionID || props.sessionId || props.session_id || props.id;
1029
1018
  const r = await runFailproofai(claudeEvent, {
1030
1019
  session_id: sessionID, cwd: directory, hook_event_name: claudeEvent,
1031
1020
  }, directory);
@@ -1,191 +1,24 @@
1
- /**
2
- * Putting a short message in front of a HUMAN, inside the agent they are using.
3
- *
4
- * A notice is not a verdict. `allow` / `deny` / `instruct` all answer "what
5
- * happens to this tool call"; a notice answers nothing — it is one line of text
6
- * for the person, delivered alongside whatever the verdict turned out to be.
7
- * That is why it is a separate field rather than a fourth decision: shaping it
8
- * into stdout beside a verdict ad hoc is how the copilot nested-vs-flat
9
- * `additionalContext` bug happened.
10
- *
11
- * ## Why the channel differs per CLI, and why it is not `additionalContext`
12
- *
13
- * Measured across all 12 CLIs by live probe. The obvious channel —
14
- * `hookSpecificOutput.additionalContext` — reaches the MODEL, not the user, and
15
- * a model is a paraphraser: over eight real Claude turns it mentioned an
16
- * informational notice 8/8, but an earlier notice that asked the agent to *act*
17
- * was disclaimed as a suspected prompt injection 6/6 ("instructions arriving
18
- * that way aren't something I'll act on unasked"). A security notice routed
19
- * through something that may decline to repeat it is not a delivered notice.
20
- *
21
- * So each CLI gets the channel that was proven to paint text on the user's
22
- * terminal, and the ones with no such channel get nothing rather than a
23
- * pretend delivery:
24
- *
25
- * claude `{"systemMessage":…}` renders `Stop says: …`; model-invisible
26
- * (a neutral probe answered `NONE`)
27
- * codex `{"systemMessage":…}` renders `• Stop (completed) says: …`
28
- * copilot exit 0 + stderr renders as a `!` session warning
29
- * factory plain stdout renders under a `Hooks Stop` block
30
- *
31
- * opencode and pi have real channels too, but they live inside the plugin shims
32
- * we ship rather than in a hook's stdout, so a private JSON field is consumed
33
- * there and handed to `showToast` / `ui.notify`. OpenClaw, cursor, devin, goose,
34
- * antigravity and hermes have no verified channel that reaches a human without
35
- * an unsupported escape hatch — they get nothing here, and their users get the
36
- * finding from `failproofai audit` and the scheduled desktop notification.
37
- */
38
- import type { IntegrationType } from "./types";
39
-
40
- /** How a notice is delivered on one CLI. */
41
- type NoticeChannel = "systemMessage" | "stderr" | "stdout" | "shim" | "none";
42
-
43
- /**
44
- * Per-CLI channel, from live probes rather than documentation.
45
- *
46
- * A CLI absent from this map delivers nothing — deliberately. Guessing a
47
- * channel produces the worst outcome available: output that looks delivered
48
- * from our side and reaches nobody, so we would report a leak as "notified"
49
- * that the user never saw.
50
- */
51
- const NOTICE_CHANNEL: Partial<Record<IntegrationType, NoticeChannel>> = {
52
- claude: "systemMessage",
53
- codex: "systemMessage",
54
- copilot: "stderr",
55
- factory: "stdout",
56
- opencode: "shim",
57
- pi: "shim",
58
- };
59
-
60
- /**
61
- * Claude truncates `systemMessage` at 4000 characters on the cloud-forwarding
62
- * path, and a notice that gets cut mid-sentence reads as a broken tool. Ours is
63
- * two lines by design; this is a backstop, not a budget.
64
- */
65
- const MAX_NOTICE_CHARS = 900;
66
-
67
- export interface ShapedNotice {
68
- /** Merged into the outcome's stdout, or empty. */
69
- stdout: string;
70
- /** Merged into the outcome's stderr, or empty. */
71
- stderr: string;
72
- }
73
-
74
- /**
75
- * Render a notice for one CLI, merging it into any verdict JSON already bound
76
- * for stdout.
77
- *
78
- * `existingStdout` matters: a Stop-gate deny already emits a JSON object on
79
- * several CLIs, and emitting a second object would produce two JSON documents
80
- * on one stream — which every host parses as a syntax error and drops, taking
81
- * the deny with it. So the notice is merged INTO that object when there is one.
82
- */
83
- export function shapeNotice(
84
- cli: IntegrationType | undefined,
85
- notice: string,
86
- existingStdout = "",
87
- ): ShapedNotice {
88
- const empty: ShapedNotice = { stdout: existingStdout, stderr: "" };
89
- if (!notice.trim() || !cli) return empty;
90
-
91
- const channel = NOTICE_CHANNEL[cli] ?? "none";
92
- const text = notice.length > MAX_NOTICE_CHARS
93
- ? notice.slice(0, MAX_NOTICE_CHARS - 1) + "…"
94
- : notice;
95
-
96
- switch (channel) {
97
- case "systemMessage": {
98
- // Merge into the verdict object when one exists, so the stream stays a
99
- // single JSON document.
100
- if (existingStdout.trim().startsWith("{")) {
101
- try {
102
- const parsed = JSON.parse(existingStdout) as Record<string, unknown>;
103
- parsed.systemMessage = text;
104
- return { stdout: JSON.stringify(parsed), stderr: "" };
105
- } catch {
106
- // Not the JSON we thought it was — leave the verdict untouched
107
- // rather than risk destroying it to add a courtesy message.
108
- return empty;
109
- }
110
- }
111
- // Anything already on stdout that we could not merge into is left
112
- // exactly as it is, and the notice is dropped instead. The alternative —
113
- // emitting our own object over the top — DESTROYS it: measured with a
114
- // JSON array on stdout, which does not start with `{`, so it fell past
115
- // the merge and was replaced wholesale. A courtesy message is never worth
116
- // more than whatever the host was already being told.
117
- if (existingStdout.trim()) return empty;
118
- return { stdout: JSON.stringify({ systemMessage: text }), stderr: "" };
119
- }
120
- case "stderr":
121
- // Exit code is NOT touched: on copilot a non-zero exit from a tool event
122
- // is a block, and a notice must never block anything.
123
- return { stdout: existingStdout, stderr: text + "\n" };
124
- case "stdout":
125
- // factory renders plain text under its Hooks block. Only safe when no
126
- // verdict JSON is already on the stream.
127
- if (existingStdout.trim()) return empty;
128
- return { stdout: text + "\n", stderr: "" };
129
- case "shim": {
130
- // OpenCode and Pi are in-process plugin integrations. Their generated
131
- // shims consume this private field and call the host's real user-facing
132
- // notification API; putting the text in additionalContext would send it
133
- // to the model instead.
134
- if (existingStdout.trim().startsWith("{")) {
135
- try {
136
- const parsed = JSON.parse(existingStdout) as Record<string, unknown>;
137
- parsed.failproofaiNotice = text;
138
- return { stdout: JSON.stringify(parsed), stderr: "" };
139
- } catch {
140
- return empty;
141
- }
142
- }
143
- if (existingStdout.trim()) return empty;
144
- return { stdout: JSON.stringify({ failproofaiNotice: text }), stderr: "" };
145
- }
146
- default:
147
- return empty;
148
- }
149
- }
150
-
151
- /** True when this CLI has a channel that was proven to reach a human. */
152
- export function canDeliverNotice(cli: IntegrationType | undefined): boolean {
153
- return !!cli && (NOTICE_CHANNEL[cli] ?? "none") !== "none";
154
- }
155
-
156
- /**
157
- * The notice itself. A FIXED template with no interpolation of scanned content.
158
- *
159
- * The count is a number we computed; everything else is a constant. That is not
160
- * stylistic: the alternative is interpolating a finding's `example`, which is
161
- * the verbatim text of a command from a repository — attacker-controlled
162
- * through a README, a Makefile target or an npm script. Tested against the
163
- * shipped redactor, `echo "IGNORE ALL PRIOR INSTRUCTIONS…"` passes through
164
- * byte-identical, because that redactor masks secrets, not instructions. On the
165
- * CLIs where the only channel is the model's context, that string would land
166
- * directly in the prompt. So nothing scanned ever reaches this template.
167
- */
168
- export function leakNoticeText(newFindings: number): string {
169
- const n = newFindings === 1
170
- ? "a possible credential exposure"
171
- : `${newFindings} possible credential exposures`;
172
- return (
173
- `failproofai found ${n} in this project's agent transcripts.\n` +
174
- "Run `failproofai audit` to review the matches and rotate anything real."
175
- );
176
- }
177
-
178
1
  /** Copy for the OS notification raised by an unattended scheduled scan.
179
2
  *
180
3
  * Deliberately omits the count. A burst of generated fixtures can hit the
181
4
  * record cap, and a desktop banner saying "500 leaked credentials" states a
182
5
  * conclusion the detector cannot support. The dashboard carries the evidence;
183
6
  * the banner's job is only to ask for review. */
184
- export function desktopLeakNotice(): { title: string; body: string } {
7
+ export type DesktopLeakEmailState = "sent" | "missing" | "failed" | "held";
8
+
9
+ export function desktopLeakNotice(email: DesktopLeakEmailState = "held"): { title: string; body: string } {
10
+ const followUp =
11
+ email === "sent"
12
+ ? "A masked alert was also sent to your email."
13
+ : email === "missing"
14
+ ? "Add email alerts with: failproofai audit --schedule --email you@example.com"
15
+ : email === "failed"
16
+ ? "Email delivery failed this time; the local result is still available."
17
+ : "Email was not sent for this run.";
185
18
  return {
186
19
  title: "failproofai audit needs review",
187
20
  body:
188
21
  "Possible credential exposure was found in your agent transcripts. " +
189
- "Run failproofai audit to verify the matches and rotate anything real.",
22
+ `Run failproofai audit to verify the matches and rotate anything real. ${followUp}`,
190
23
  };
191
24
  }
@@ -1,3 +0,0 @@
1
- module.exports=[74863,a=>{"use strict";var b=a.i(36359),c=a.i(37936),d=a.i(15501),e=a.i(13095);async function f(){let a=(0,d.readDashboardCache)();if(!a){let a=(0,d.readDashboardCacheMeta)();return{status:"empty",expired:null!==a,expiredAt:a?.cachedAt??null}}return{status:"cached",cachedAt:a.cachedAt,params:a.params,result:a.result}}(0,e.ensureServerEntryExports)([f]),(0,c.registerServerReference)(f,"0048814305f7563284b0e6413ac4ea842889a1d6eb",null);var g=a.i(66680),h=a.i(2157),i=a.i(50227),j=a.i(75818),k=a.i(8674);function l(a){try{if(!(0,h.existsSync)(a))return null;return JSON.parse((0,h.readFileSync)(a,"utf8"))}catch{return null}}function m(a){let b=l((0,j.auditLeakIdentityFile)(a));if(b&&"string"==typeof b.salt&&b.salt.length>=32)return{salt:b.salt,dismissed:b.dismissed??[],reportViewedAt:"number"==typeof b.reportViewedAt&&Number.isFinite(b.reportViewedAt)?b.reportViewedAt:void 0};let c={salt:(0,g.randomBytes)(32).toString("hex"),dismissed:[]};return n(c,a),c}function n(a,b){let c=(0,j.auditLeakIdentityFile)(b);try{return(0,h.mkdirSync)((0,i.dirname)(c),{recursive:!0}),(0,k.writeJsonAtomically)(c,a,{mode:384}),!0}catch{return!1}}function o(a){var b;if(!a||"object"!=typeof a||Array.isArray(a)||"string"!=typeof a.id||(b=a.id,!/^[0-9a-f]{16}$/.test(b)))return null;let c=a.fingerprint;if(!c||"object"!=typeof c||"string"!=typeof c.display||"string"!=typeof c.label)return null;let d=Array.isArray(a.sightings)?a.sightings.filter(a=>{if(!a||"object"!=typeof a)return!1;let b=a.mechanism;return!!b&&"object"==typeof b&&"string"==typeof b.summary}):[];return{...a,fingerprint:{display:c.display,label:c.label,length:"number"==typeof c.length&&Number.isFinite(c.length)?c.length:0,attributed:!0===c.attributed},name:"string"==typeof a.name?a.name:null,firstSeen:"string"==typeof a.firstSeen?a.firstSeen:"",lastSeen:"string"==typeof a.lastSeen?a.lastSeen:"",occurrences:"number"==typeof a.occurrences&&Number.isFinite(a.occurrences)?a.occurrences:d.length,sightings:d}}var p=a.i(21572);async function q(){var a;let b,c=(a=function(){let a=m(void 0),b=l((0,j.auditLeaksFile)(void 0));if(!b||1!==b.schemaVersion||!Array.isArray(b.findings))return{schemaVersion:1,salt:a.salt,updatedAt:new Date().toISOString(),findings:[]};let c=b.findings.map(o).filter(a=>null!==a);return{...b,findings:c,salt:a.salt}}(),b=new Set(m(void 0).dismissed),a.findings.filter(a=>!b.has(a.id)).sort((a,b)=>a.lastSeen<b.lastSeen?1:a.lastSeen>b.lastSeen?-1:0));!function(a=Date.now()){try{let b=m(void 0);(0,k.writeJsonAtomically)((0,j.auditLeakIdentityFile)(void 0),{...b,reportViewedAt:a},{mode:384})}catch{}}();let d=c.map(a=>{let b=a.sightings?.[a.sightings.length-1];return{id:a.id,display:a.fingerprint?.display??"[credential]",label:a.fingerprint?.label??"secret",length:a.fingerprint?.length??0,attributed:a.fingerprint?.attributed===!0,name:a.name,cli:b?.cli??"unknown",project:b?.cwd??"unknown",lastSeen:a.lastSeen,firstSeen:a.firstSeen,mechanism:b?.mechanism?.summary??"seen in a transcript",direction:b?.mechanism?.direction??"result",occurrences:a.occurrences,sessions:new Set((a.sightings??[]).map(a=>a.sessionId)).size}});return d.sort((a,b)=>Date.parse(b.lastSeen)-Date.parse(a.lastSeen)),{rows:d,notify:(0,p.readConfig)().audit.notify}}async function r(a){let b;return!!(b=m(void 0)).dismissed.includes(a)||(b.dismissed.push(a),n(b,void 0))}async function s(a){try{return(0,p.updateConfig)({audit:{notify:a}}),!0}catch{return!1}}(0,e.ensureServerEntryExports)([q,r,s]),(0,c.registerServerReference)(q,"00fd330814bcefc936e301ec49d66099ed614ecf67",null),(0,c.registerServerReference)(r,"4097c062c6b1d13b1d6f2a64af27881249bbf9d989",null),(0,c.registerServerReference)(s,"409ed106c4be852cd917c107309fc8fd09d79a0beb",null),a.s([],37550),a.i(37550),a.s(["000c85ddbcc0c76485115f8fdbd1792f16d26c97bb",()=>b.getTelemetryConfig,"0048814305f7563284b0e6413ac4ea842889a1d6eb",0,f,"00fd330814bcefc936e301ec49d66099ed614ecf67",0,q,"4097c062c6b1d13b1d6f2a64af27881249bbf9d989",0,r,"409ed106c4be852cd917c107309fc8fd09d79a0beb",0,s],74863)},2726,a=>{a.v(b=>Promise.all(["server/chunks/ssr/[externals]__0vmspq5._.js","server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js"].map(b=>a.l(b))).then(()=>b(74493)))}];
2
-
3
- //# sourceMappingURL=_0oq1dh6._.js.map
@@ -1 +0,0 @@
1
- (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,86616,29054,e=>{"use strict";function t(e,a={},s=15e3){let n=AbortSignal.timeout(s),i=a.signal?AbortSignal.any([a.signal,n]):n;return fetch(e,{...a,signal:i})}function a(e){if(null===e||"object"!=typeof e)return!1;let t=e.name;return"AbortError"===t||"TimeoutError"===t}e.s(["fetchWithTimeout",0,t,"isAbortError",0,a],29054);class s extends Error{kind;constructor(e,t){super(t),this.kind=e,this.name="RerunError"}}async function n(e){try{let a=await t("/api/audit/run",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({cli:e.cli.length>0?e.cli:void 0,since:"all"===e.since?void 0:e.since,noCache:!!e.noCache||void 0})});if(!a.ok&&409!==a.status){let e=await a.text().catch(()=>"");throw console.error("audit run failed:",a.status,e),new s("post_failed",`audit run failed (${a.status})`)}}catch(e){if(e instanceof s)throw e;throw console.error("audit run request failed:",e),new s(a(e)?"timeout":"network","audit run request failed")}let n=0;for(;;){await new Promise(e=>setTimeout(e,1e3));let e=null;try{let a=await t("/api/audit/status",{cache:"no-store"});a.ok&&(e=await a.json())}catch{}if(null===e){if(++n>=10)throw new s("network","audit status poll lost the server");continue}if(n=0,!e.running){if(e.error)throw new s("post_failed",e.error);return}}}e.s(["RerunError",0,s,"triggerRun",0,n],86616)},93237,e=>{"use strict";var t=e.i(43476),a=e.i(71645),s=e.i(95187);let n=(0,s.createServerReference)("00d97410f220c9506cdadd420812b2c7ab41b6dee8",s.callServer,void 0,s.findSourceMapURL,"getScheduledAuditAction"),i=(0,s.createServerReference)("4070638273d8ce919cc1cb3a9c56f9581269fad56d",s.callServer,void 0,s.findSourceMapURL,"setAutoAuditAction"),r=(0,s.createServerReference)("40b2805baae56152c00aa41d8f14da989078e7227d",s.callServer,void 0,s.findSourceMapURL,"setAuditIntervalAction");var l=e.i(86616),o=e.i(12344),c=e.i(29054);let d=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;function u(e){if((0,c.isAbortError)(e))return"request timed out. check your network and try again.";let t=e instanceof Error?e.message:String(e);return`network error: ${t}`}function h({open:e,headline:s="where to route the reminder?",subhead:n="we'll send a one-time code to confirm.",onClose:i,onAuthed:r,source:l="unknown"}){let{capture:m}=(0,o.usePostHog)(),[f,b]=(0,a.useState)({kind:"email",error:null}),[p,x]=(0,a.useState)(!1),v=(0,a.useRef)(!1),g=(0,a.useRef)(null),y=(0,a.useRef)(null);(0,a.useEffect)(()=>{e&&(b({kind:"email",error:null}),x(!1),v.current=!1,m("audit_auth_dialog_opened",{source:l}))},[m,e,l]);let w=(0,a.useRef)(!1);(0,a.useEffect)(()=>{if(e){w.current=!0;return}w.current&&!v.current&&m("audit_auth_dialog_dismissed",{source:l,step:f.kind}),w.current=!1},[m,e,l,f.kind]),(0,a.useEffect)(()=>{if(!e)return;let t=setTimeout(()=>{"email"===f.kind?g.current?.focus():"code"===f.kind&&y.current?.focus()},50);return()=>clearTimeout(t)},[e,f.kind]),(0,a.useEffect)(()=>{if(!e)return;let t=e=>{"Escape"!==e.key||p||i()};return window.addEventListener("keydown",t),()=>window.removeEventListener("keydown",t)},[e,p,i]);let j="code"===f.kind&&f.resendIn>0;(0,a.useEffect)(()=>{if(!j)return;let e=setInterval(()=>{b(e=>"code"===e.kind?{...e,resendIn:Math.max(0,e.resendIn-1)}:e)},1e3);return()=>clearInterval(e)},[j]);let k=(0,a.useCallback)(async(e,t={})=>{let{isResend:a=!1}=t,s=e=>{a?b(t=>"code"===t.kind?{...t,error:e}:t):b({kind:"email",error:e})};x(!0);try{let t=await (0,c.fetchWithTimeout)("/api/auth/login-request",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({email:e})}),a=await t.json().catch(()=>({}));if(!t.ok){let e=a.message??"could not send code.";"rate_limited"===a.code&&void 0!==a.retry_after_secs?e=`too many tries. wait ${a.retry_after_secs}s and try again.`:"upstream_unreachable"===a.code&&(e="api-server unreachable. check your network."),s(e);return}b({kind:"code",email:e,error:null,expiresIn:a.expires_in??600,resendIn:a.resend_available_in??30})}catch(e){s(u(e))}finally{x(!1)}},[]),N=(0,a.useCallback)(async(e,t)=>{x(!0);try{let a=await (0,c.fetchWithTimeout)("/api/auth/login-verify",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({email:e,code:t})}),s=await a.json().catch(()=>({}));if(!a.ok||!s.authenticated||!s.user){let e=s.message??"invalid code.";"invalid_code"===s.code&&(e="wrong code, or it expired. try again."),b(t=>"code"===t.kind?{...t,error:e}:t);return}v.current=!0,m("audit_auth_dialog_succeeded",{source:l}),b({kind:"done",user:s.user}),r(s.user)}catch(t){let e=u(t);b(t=>"code"===t.kind?{...t,error:e}:t)}finally{x(!1)}},[m,r,l]),S=(0,a.useCallback)(async e=>{if(e.preventDefault(),p||"email"!==f.kind)return;let t=String(new FormData(e.currentTarget).get("email")??"").trim().toLowerCase();d.test(t)?await k(t):b({kind:"email",error:"that doesn't look like an email."})},[p,f,k]),C=(0,a.useCallback)(async e=>{if(e.preventDefault(),p||"code"!==f.kind)return;let t=String(new FormData(e.currentTarget).get("code")??"").trim();t.length<4||t.length>12?b(e=>"code"===e.kind?{...e,error:"code is 4–12 characters."}:e):await N(f.email,t)},[p,f,N]),M=(0,a.useCallback)(async()=>{"code"!==f.kind||f.resendIn>0||p||await k(f.email,{isResend:!0})},[f,p,k]);return e?(0,t.jsx)("div",{className:"auth-dialog-backdrop",role:"dialog","aria-modal":"true","aria-labelledby":"auth-dialog-title",onClick:e=>{p||e.target!==e.currentTarget||i()},children:(0,t.jsxs)("div",{className:"auth-dialog",children:[(0,t.jsx)("button",{type:"button",className:"auth-close",onClick:i,disabled:p,"aria-label":"close",children:"×"}),(0,t.jsx)("h2",{id:"auth-dialog-title",className:"auth-headline",children:s}),"email"===f.kind&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("p",{className:"auth-sub",children:n}),(0,t.jsxs)("form",{onSubmit:S,className:"auth-form",children:[(0,t.jsx)("input",{ref:g,id:"auth-dialog-email",name:"email",type:"email",autoComplete:"email",inputMode:"email",spellCheck:!1,placeholder:"you@yourdomain.com",disabled:p,className:"auth-input",required:!0}),f.error&&(0,t.jsx)("div",{className:"auth-error",children:f.error}),(0,t.jsxs)("div",{className:"auth-actions",children:[(0,t.jsx)("button",{type:"submit",className:"auth-btn primary",disabled:p,children:p?"sending…":"send code"}),(0,t.jsx)("button",{type:"button",className:"auth-btn",onClick:i,disabled:p,children:"cancel"})]})]})]}),"code"===f.kind&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("p",{className:"auth-sub",children:["code sent to ",(0,t.jsx)("span",{className:"auth-email",children:f.email}),". expires in ",Math.ceil(f.expiresIn/60)," min."]}),(0,t.jsxs)("form",{onSubmit:C,className:"auth-form",children:[(0,t.jsx)("input",{ref:y,id:"auth-dialog-code",name:"code",type:"text",inputMode:"numeric",autoComplete:"one-time-code",spellCheck:!1,placeholder:"123456",disabled:p,className:"auth-input auth-input-code",maxLength:12,required:!0}),f.error&&(0,t.jsx)("div",{className:"auth-error",children:f.error}),(0,t.jsxs)("div",{className:"auth-actions",children:[(0,t.jsx)("button",{type:"submit",className:"auth-btn primary",disabled:p,children:p?"verifying…":"verify"}),(0,t.jsx)("button",{type:"button",className:"auth-btn",onClick:M,disabled:p||f.resendIn>0,children:f.resendIn>0?`resend in ${f.resendIn}s`:"resend code"})]}),(0,t.jsx)("button",{type:"button",className:"auth-back",onClick:()=>b({kind:"email",error:null}),disabled:p,children:"← use a different email"})]})]}),"done"===f.kind&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("p",{className:"auth-sub",children:[(0,t.jsx)("span",{className:"auth-ok",children:"✓"})," signed in as"," ",(0,t.jsx)("span",{className:"auth-email",children:f.user.email}),"."]}),(0,t.jsx)("div",{className:"auth-actions",children:(0,t.jsx)("button",{type:"button",className:"auth-btn primary",onClick:i,children:"continue"})})]})]})}):null}var m=e.i(55574),f=e.i(34713);function b(e){return new Date(e).toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}function p(e,t){let a=e-t;if(a<=0)return"now";let s=Math.floor(a/864e5),n=Math.floor(a%864e5/36e5);return s>0?n>0?`${s}d ${n}h`:`${s}d`:n>0?`${n}h`:`${Math.max(1,Math.floor(a/6e4))}m`}function x(e){return e.toLocaleString("en-US")}function v({label:e,value:a,sub:s,tone:n}){return(0,t.jsxs)("div",{className:"set-stat",children:[(0,t.jsx)("div",{className:"set-stat-label",children:e}),(0,t.jsx)("div",{className:`set-stat-value${n?` ${n}`:""}`,children:a}),(0,t.jsx)("div",{className:"set-stat-sub",children:s??" "})]})}function g({lastRunAtMs:e,nextDueAtMs:a,now:s}){if(null==e||null==a||a<=e)return null;let n=Math.min(100,Math.max(0,(s-e)/(a-e)*100));return(0,t.jsxs)("div",{className:"tape","aria-hidden":"true",children:[(0,t.jsxs)("div",{className:"tape-rail",children:[(0,t.jsx)("div",{className:"tape-fill",style:{width:`${n}%`}}),(0,t.jsx)("div",{className:"tape-now",style:{left:`${n}%`}})]}),(0,t.jsxs)("div",{className:"tape-ends",children:[(0,t.jsxs)("span",{children:["last scan · ",b(e)]}),(0,t.jsxs)("span",{className:"tape-next",children:["next · ",p(a,s)]})]})]})}function y({enabled:e,onChange:a,disabled:s,label:n}){return(0,t.jsx)("button",{type:"button",role:"switch","aria-checked":e,"aria-label":n,disabled:s,onClick:a,className:"sw","data-on":e?"true":"false",children:(0,t.jsx)("span",{className:"sw-knob"})})}let w=[{label:"reads",body:"every session transcript on disk — your prompts, the files your agents read and wrote, and command output."},{label:"runs",body:"entirely on this machine. the transcripts never leave it."},{label:"sends",body:"counts, redacted examples, and this machine's name — and only when a scan finds something harmful."}];e.s(["default",0,function({initial:e}){let[s,o]=(0,a.useState)(e),[c,d]=(0,a.useState)(e?.auto??!1),[u,j]=(0,a.useState)(e?.intervalDays??7),[k,N]=(0,a.useState)(!1),[S,C]=(0,a.useState)(!1),[M,$]=(0,a.useState)(!1),[E,A]=(0,a.useState)(null===e),[D,R]=(0,a.useState)(0),_=(0,a.useRef)(!0),T=(0,a.useRef)(null!==e),I=(0,a.useCallback)(async()=>{try{let e=await n();if(!_.current)return;o(e),T.current=!0,d(e.auto),j(e.intervalDays),R(Date.now()),A(!1)}catch{_.current&&!T.current&&A(!0)}},[]);(0,a.useEffect)(()=>(_.current=!0,I(),()=>{_.current=!1}),[I]),(0,a.useEffect)(()=>{let e=()=>void I();return window.addEventListener("focus",e),document.addEventListener("visibilitychange",e),()=>{window.removeEventListener("focus",e),document.removeEventListener("visibilitychange",e)}},[I]);let L=s?.signedInAs??null,F=s?.daemon==="running",O=s?.daemon==="unsupported-platform",P=s?.schedule??null,q=s?.lastScan??null,U=P?.lastExitCode!=null&&0!==P.lastExitCode&&75!==P.lastExitCode,B=(0,a.useCallback)(async()=>{N(!0);try{let e=await i(!0);if(!e.ok){if(d(!1),"unreachable"===e.reason)return void(0,m.toast)("could not reach the server. check your connection and try again.");await I(),$(!0),(0,m.toast)("that sign-in expired. one more code and it's on.");return}d(e.auto),(0,m.toast)("scheduled audits on."),await I()}catch{d(!1),(0,m.toast)("could not turn that on.")}finally{N(!1)}},[I]),J=(0,a.useCallback)(async()=>{if(c){N(!0);try{let e=await i(!1);e.ok&&d(e.auto),(0,m.toast)("scheduled audits off."),await I()}catch{(0,m.toast)("could not turn that off.")}finally{N(!1)}return}L?await B():$(!0)},[c,B,I,L]),W=(0,a.useCallback)(async e=>{N(!0);try{let t=await r(e);j(t.intervalDays),o(e=>e?{...e,intervalDays:t.intervalDays}:e),(0,m.toast)(`scanning every ${t.intervalDays} day${1===t.intervalDays?"":"s"}.`)}catch{j(s?.intervalDays??7),(0,m.toast)("could not save that.")}finally{N(!1)}},[s?.intervalDays]),K=(0,a.useCallback)(async()=>{N(!0);try{await fetch("/api/auth/logout",{method:"POST"}),(0,m.toast)("signed out. scans continue; digests pause."),await I()}catch{(0,m.toast)("could not sign out.")}finally{N(!1)}},[I]),H=(0,a.useCallback)(async()=>{if(!S){C(!0);try{await (0,l.triggerRun)({cli:[],since:"all",noCache:!1}),(0,m.toast)("scan complete."),await I()}catch(e){(0,m.toast)(e instanceof l.RerunError&&"timeout"===e.kind?"the scan is taking a while — it will finish in the background.":"the scan could not be completed.")}finally{C(!1)}}},[I,S]),z=(0,a.useCallback)(async e=>{$(!1),await I(),await B()},[B,I]),G=s?F?{value:"running",sub:null!=s.daemonStartedAtMs&&D>0?`up ${function(e){let t=Math.floor(e/864e5);if(t>0)return`${t}d`;let a=Math.floor(e/36e5);return a>0?`${a}h`:`${Math.max(1,Math.floor(e/6e4))}m`}(Math.max(0,D-s.daemonStartedAtMs))}`:"",tone:"ok"}:O?{value:"unavailable",sub:"not on this platform"}:"not-installed"===s.daemon?{value:"not installed",sub:"run failproofai config",tone:"warn"}:"unknown"===s.daemon?{value:"unknown",sub:"could not read it"}:{value:"stopped",sub:"run failproofai config",tone:"warn"}:{value:"…",sub:"reading"},Q=c?P?.nextDueAtMs==null?{value:"pending",sub:"after the next scan"}:{value:D>0?p(P.nextDueAtMs,D):"—",sub:b(P.nextDueAtMs)}:{value:"off",sub:"nothing scheduled"},V=q?{value:(0,f.formatRelativeTime)(Date.parse(q.finishedAt)),sub:null!=q.sessionsScanned?`${x(q.sessionsScanned)} sessions`:"scanned"}:{value:"none yet",sub:"run one below"},X=q?null==q.findings?{value:"—",sub:"unreadable"}:{value:x(q.findings),sub:"this scan",...q.findings>0?{tone:"warn"}:{}}:{value:"—",sub:"no scan yet"};return(0,t.jsxs)("main",{className:"report",children:[(0,t.jsxs)("section",{className:"section set-sec",children:[(0,t.jsxs)("div",{className:"set-mast",children:[(0,t.jsx)("h1",{className:"set-title",children:"settings"}),(0,t.jsx)("p",{className:"set-lede",children:"keeping watch, so you don't have to."})]}),E?(0,t.jsx)("div",{className:"set-console",children:(0,t.jsx)("div",{className:"set-cell",children:(0,t.jsxs)("p",{className:"set-warn",children:["could not read this machine's settings. check that the dashboard can reach ",(0,t.jsx)("code",{children:"~/.failproofai/config.json"}),"."]})})}):(0,t.jsxs)("div",{className:"set-console",children:[(0,t.jsxs)("div",{className:"set-stats",children:[(0,t.jsx)(v,{label:"daemon",...G}),(0,t.jsx)(v,{label:"next scan",...Q}),(0,t.jsx)(v,{label:"last scan",...V}),(0,t.jsx)(v,{label:"findings",...X})]}),(0,t.jsxs)("div",{className:"set-cols",children:[(0,t.jsxs)("div",{className:"set-cell set-config",children:[(0,t.jsx)("h2",{className:"set-cell-title",children:"scheduled audit"}),(0,t.jsxs)("div",{className:"set-row set-row-main",children:[(0,t.jsx)(y,{enabled:c,disabled:k||null===s,onChange:()=>void J(),label:c?"turn off scheduled audits":"turn on scheduled audits"}),(0,t.jsxs)("div",{className:"set-row-copy",children:[(0,t.jsx)("span",{className:"set-strong",children:c?"scanning on a schedule.":"scan on a schedule."}),(0,t.jsx)("span",{className:"set-dim",children:c?"you'll get an email only when a scan finds something.":"off. nothing runs and nothing is sent."})]})]}),(0,t.jsxs)("div",{className:"set-row set-row-interval",children:[(0,t.jsx)("span",{className:"set-dim",children:"scan every"}),(0,t.jsx)("input",{type:"number",className:"set-num",min:1,max:90,value:u,disabled:k||null===s,"aria-label":"days between scheduled scans",onChange:e=>j(Number(e.target.value)),onBlur:e=>{let t=Number(e.target.value);Number.isFinite(t)?t!==s?.intervalDays&&W(t):j(s?.intervalDays??7)}}),(0,t.jsx)("span",{className:"set-dim",children:"days"}),(0,t.jsxs)("span",{className:"set-hint",children:[1,"–",90]})]}),(0,t.jsx)("div",{className:"set-rule"}),(0,t.jsx)("div",{className:"set-row set-row-identity",children:L?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("span",{className:"set-dim",children:"reports go to"}),(0,t.jsx)("span",{className:"set-email",children:L.email}),(0,t.jsx)("button",{type:"button",className:"set-link",disabled:k,onClick:()=>void K(),children:"sign out"})]}):c?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("span",{className:"set-warn",children:"signed out — scans continue, digests are paused."}),(0,t.jsx)("button",{type:"button",className:"set-link",disabled:k,onClick:()=>$(!0),children:"sign in to resume"})]}):(0,t.jsx)("span",{className:"set-dim",children:"turning this on asks for an email, so there is somewhere to send the report."})}),c&&s&&!F&&(0,t.jsx)("p",{className:"set-warn",children:O?"the background service isn't available on this platform, so scheduled scans can't run here. you can still run one now.":"not-installed"===s.daemon?"the background service isn't installed, so nothing will run on the timer. install it with `failproofai config`.":"the background service is stopped, so nothing will run until it starts. repair it with `failproofai config`."}),U&&(0,t.jsxs)("p",{className:"set-warn",children:["the last scheduled scan exited ",P?.lastExitCode,". the next one will still run."]}),(0,t.jsx)("div",{className:"set-actions",children:(0,t.jsx)("button",{type:"button",className:"btn btn-press",disabled:S,onClick:()=>void H(),children:S?"[ scanning… ]":"[ run a scan now ]"})})]}),(0,t.jsxs)("div",{className:"set-cell set-how",children:[(0,t.jsx)("h2",{className:"set-cell-title",children:"how it works"}),(0,t.jsx)("dl",{className:"set-how-list",children:w.map(e=>(0,t.jsxs)("div",{className:"set-how-row",children:[(0,t.jsx)("dt",{className:"set-how-label",children:e.label}),(0,t.jsx)("dd",{className:"set-how-body",children:e.body})]},e.label))})]})]})]}),c&&D>0&&(0,t.jsx)(g,{lastRunAtMs:P?.lastRunAtMs??null,nextDueAtMs:P?.nextDueAtMs??null,now:D})]}),(0,t.jsx)(h,{open:M,source:"settings_scheduled_audit",headline:"where should the report go?",subhead:"we'll send a one-time code to confirm.",onClose:()=>$(!1),onAuthed:e=>void z(e)})]})}],93237)},34713,e=>{"use strict";e.s(["formatDuration",0,function(e){if(e<1e3)return`${e}ms`;let t=Math.round(e/100);if(t<600)return`${(t/10).toFixed(1)}s`;let a=Math.round(e/1e3);if(a<3600){let e=Math.floor(a/60);return`${e}m ${a%60}s`}let s=Math.round(e/6e4),n=Math.floor(s/60);return`${n}h ${s%60}m`},"formatRelativeTime",0,function(e){let t=Date.now()-e;return t<6e4?`${Math.max(1,Math.floor(t/1e3))}s ago`:t<36e5?`${Math.floor(t/6e4)}m ago`:t<864e5?`${Math.floor(t/36e5)}h ago`:`${Math.floor(t/864e5)}d ago`}])}]);