failproofai 1.0.0 → 1.0.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +0 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +5 -5
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/routes-manifest.json +0 -6
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +4 -3
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -3
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +3 -3
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js +2 -2
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +4 -3
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js +3 -2
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -3
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +5 -3
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +4 -3
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/server/app/policies/page.js +6 -5
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +5 -4
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -4
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +5 -4
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +6 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +0 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05ja_05._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0nne229._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ny9q5d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hstmft._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p8poxd._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0jx6cp6._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0f-x2b0._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1f290jp._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0hwvl7e._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1g2ji1t._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1ctjdzl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03eykvd._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07175vb._.js → [root-of-the-server]__07oh8b5._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cxe_2_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g2rg69._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0m98kzy._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0sjzdjg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0v5_91z._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-a3135._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1hvwiw8._.js → [root-of-the-server]__11z_299._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apc90t._.js → [root-of-the-server]__15c_z97._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1wmno1q._.js → [root-of-the-server]__17d_ffl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e5g_ur._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1hmqefd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/_04j57fb._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_05enntk._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0hom1s1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1nq-35m._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/_1q7j05x._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +15 -15
- package/.next/standalone/.next/static/chunks/00q6cfzuzpc85.js +1 -0
- package/.next/standalone/.next/static/chunks/{0zm3_8jpqitxy.js → 054eg7y_cjyf9.js} +22 -22
- package/.next/standalone/.next/static/chunks/{2ouq44jv_0x04.js → 0mjufa_0xxz75.js} +1 -1
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +1 -0
- package/.next/standalone/.next/static/chunks/{0zi8zjav5a4rq.js → 1-oz4co31nvc5.js} +1 -1
- package/.next/standalone/.next/static/chunks/13--2yvqek_s1.js +1 -0
- package/.next/standalone/.next/static/chunks/136ldjmzxhush.js +1 -0
- package/.next/standalone/.next/static/chunks/18fqy3doo2ial.js +6 -0
- package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
- package/.next/standalone/.next/static/chunks/{31k3yxpy57_fy.js → 1o5sxp_mdaoci.js} +1 -1
- package/.next/standalone/.next/static/chunks/29-iqnp9v_axt.css +1 -0
- package/.next/standalone/.next/static/chunks/{2676v-lyzgo91.js → 2es4z1ei90vsv.js} +1 -1
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
- package/.next/standalone/.next/static/chunks/3peix2vxzj7eh.js +1 -0
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +1 -0
- package/.next/standalone/.next/static/chunks/{0km-jn9o5xyqs.css → 3wq9x0c-7ld5d.css} +1 -1
- package/.next/standalone/app/actions/get-scheduled-audit.ts +65 -6
- package/.next/standalone/app/actions/update-scheduled-audit.ts +90 -17
- package/.next/standalone/app/api/auth/status/route.ts +7 -17
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +2 -6
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +101 -281
- package/.next/standalone/app/audit/_components/empty-state.tsx +2 -2
- package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
- package/.next/standalone/app/audit/audit-styles.css +150 -26
- package/.next/standalone/app/globals.css +39 -1
- package/.next/standalone/app/settings/page.tsx +38 -21
- package/.next/standalone/app/settings/settings-client.tsx +583 -392
- package/.next/standalone/app/settings/settings.css +354 -0
- package/.next/standalone/components/navbar.tsx +33 -4
- package/.next/standalone/lib/auth/api-server-client.ts +51 -29
- package/.next/standalone/lib/auth/auth-store.ts +44 -59
- package/.next/standalone/package.json +12 -10
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +6 -0
- package/bin/failproofaid-shim.mjs +0 -0
- package/dist/cli.mjs +1889 -426
- package/dist/worker.mjs +27 -5
- package/lib/auth/api-server-client.ts +51 -29
- package/lib/auth/auth-store.ts +44 -59
- package/package.json +12 -10
- package/scripts/docs-audit.ts +493 -0
- package/scripts/translate-docs/cli.ts +16 -2
- package/scripts/translate-docs/mdx-translator.ts +6 -1
- package/scripts/translate-docs/readme-translator.ts +4 -2
- package/src/audit/cli-login.ts +330 -0
- package/src/audit/cli.ts +124 -6
- package/src/audit/dashboard-cache.ts +44 -8
- package/src/audit/harm-report.ts +260 -0
- package/src/audit/machine-store.ts +120 -0
- package/src/audit/redact-example.ts +312 -0
- package/src/audit/report-harm.ts +167 -0
- package/src/audit/schedule-cli.ts +352 -0
- package/src/hooks/builtin-policies.ts +26 -0
- package/src/hooks/daemon-service.ts +76 -0
- package/src/hooks/fp-config.ts +96 -5
- package/src/hooks/fp-home.ts +118 -8
- package/src/hooks/fp-reset.ts +79 -9
- package/src/hooks/migrations.ts +241 -4
- package/src/hooks/tui.ts +61 -3
- package/.next/standalone/.next/server/app/api/auth/reminder/route/app-paths-manifest.json +0 -3
- package/.next/standalone/.next/server/app/api/auth/reminder/route/build-manifest.json +0 -12
- package/.next/standalone/.next/server/app/api/auth/reminder/route/server-reference-manifest.json +0 -4
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +0 -9
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.map +0 -5
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +0 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route_client-reference-manifest.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-xggru._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00ds05_._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1dxagv_._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fgqj1_._.js +0 -3
- package/.next/standalone/.next/server/chunks/_084toou._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0g4t8c9._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0km521a._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0t8kefc._.js +0 -3
- package/.next/standalone/.next/server/chunks/_16dwkfn._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1phm9ui._.js +0 -3
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_auth_reminder_route_actions_1kjgxf8.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dk8r_l._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kviq2m._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ogyhu0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-rfu32._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mmefsr._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1t62w37._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_032wo7w._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_08840em._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0gl2ujp._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j0ggkc._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0kbv5-q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0r1jgvp._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0t5xq1b._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0w_be0g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_15gba46._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_1qyhb9r._.js +0 -23
- package/.next/standalone/.next/server/chunks/ssr/_1vel2ey._.js +0 -3
- package/.next/standalone/.next/static/chunks/0wd503tv801gy.js +0 -1
- package/.next/standalone/.next/static/chunks/1hfocb0sdl85c.js +0 -1
- package/.next/standalone/.next/static/chunks/1v1gqvpjk62jm.js +0 -1
- package/.next/standalone/.next/static/chunks/2kopjq_or743g.js +0 -6
- package/.next/standalone/.next/static/chunks/2uvhw57mf9lg_.css +0 -2
- package/.next/standalone/.next/static/chunks/2ynl0-6aqng_b.js +0 -1
- package/.next/standalone/.next/static/chunks/3xvtgz6arztp_.js +0 -1
- package/.next/standalone/app/api/auth/reminder/route.ts +0 -213
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email-OTP sign-in, in the terminal.
|
|
3
|
+
*
|
|
4
|
+
* The dashboard has had this since the beginning, as two API routes. The CLI
|
|
5
|
+
* had nothing, which meant the one surface that works on a headless box could
|
|
6
|
+
* not do the one thing scheduling requires. This is the same flow through the
|
|
7
|
+
* same functions — `requestLoginCode` / `verifyLoginCode` from
|
|
8
|
+
* `api-server-client`, `writeAuth` from `auth-store` — writing the same
|
|
9
|
+
* `~/.failproofai/audit/session.json`.
|
|
10
|
+
*
|
|
11
|
+
* Sharing the store is what makes "the CLI and the dashboard are always in
|
|
12
|
+
* sync" true by construction rather than by discipline: there is one file and
|
|
13
|
+
* one writer, so signing in here shows up there on the next read, and signing
|
|
14
|
+
* out there ends the session the scheduled audit was going to report under.
|
|
15
|
+
*/
|
|
16
|
+
import { AuthApiError, requestLoginCode, verifyLoginCode } from "../../lib/auth/api-server-client";
|
|
17
|
+
import {
|
|
18
|
+
authFromTokenResponse,
|
|
19
|
+
readAuth,
|
|
20
|
+
writeAuth,
|
|
21
|
+
type StoredAuth,
|
|
22
|
+
} from "../../lib/auth/auth-store";
|
|
23
|
+
import {
|
|
24
|
+
ANSI_RESET,
|
|
25
|
+
BAR,
|
|
26
|
+
colorsEnabled,
|
|
27
|
+
intro,
|
|
28
|
+
outro,
|
|
29
|
+
promptText,
|
|
30
|
+
step,
|
|
31
|
+
stepOpen,
|
|
32
|
+
} from "../hooks/tui";
|
|
33
|
+
|
|
34
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The api-server's own bound on a submitted code (`auth/models.rs`). Matched
|
|
38
|
+
* here so a paste that is obviously too long is rejected at the prompt, where
|
|
39
|
+
* it can be retyped, rather than by the server — which answers
|
|
40
|
+
* `validation_error` rather than `invalid_code`, a distinction the retry loop
|
|
41
|
+
* below treats as fatal.
|
|
42
|
+
*/
|
|
43
|
+
const CODE_MIN = 4;
|
|
44
|
+
const CODE_MAX = 12;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The spine a prompt hangs off, so the two questions sit inside the same frame
|
|
48
|
+
* `intro`/`outro` draw. Empty when colour is off or output is piped: the frame
|
|
49
|
+
* is decoration, and a log file should not collect box-drawing characters.
|
|
50
|
+
*/
|
|
51
|
+
function spine(): string {
|
|
52
|
+
return colorsEnabled(process.stdout)
|
|
53
|
+
? `${ANSI_DIM_BAR}${BAR}${ANSI_RESET} `
|
|
54
|
+
: "";
|
|
55
|
+
}
|
|
56
|
+
const ANSI_DIM_BAR = "\x1B[2m";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Codes are numeric (`auth/otp.rs` generates digits only), so anything else in
|
|
60
|
+
* the field is packaging: "Your failproof code is 123456", a copied "123 456",
|
|
61
|
+
* a stray trailing space from a double-click selection. Keeping the digits is
|
|
62
|
+
* the difference between a paste that works and one that costs a fresh email.
|
|
63
|
+
*
|
|
64
|
+
* Applied only when the input HAS digits and something else — a field of pure
|
|
65
|
+
* digits passes through untouched, so a genuinely wrong code is still reported
|
|
66
|
+
* as wrong rather than silently rewritten into a different one.
|
|
67
|
+
*/
|
|
68
|
+
export function extractCode(raw: string): string {
|
|
69
|
+
const trimmed = raw.trim();
|
|
70
|
+
if (/^\d+$/.test(trimmed)) return trimmed;
|
|
71
|
+
const runs = trimmed.match(/\d+/g) ?? [];
|
|
72
|
+
if (runs.length === 0) return trimmed;
|
|
73
|
+
// A run long enough to BE a code wins outright.
|
|
74
|
+
//
|
|
75
|
+
// Joining every digit in the line was the whole rule, and the prompt's own
|
|
76
|
+
// hint ("paste the whole line if you like") walks straight into it: the real
|
|
77
|
+
// message reads `Your failproof code is 123456 (expires in 10 minutes)`, so
|
|
78
|
+
// the join produced `12345610` — eight digits, which passes the 4–12
|
|
79
|
+
// validator, reaches the server, and burns an attempt on a code nobody typed.
|
|
80
|
+
// Anything the sentence adds after the code is short; the code is not.
|
|
81
|
+
const whole = runs.find((run) => run.length >= CODE_MIN);
|
|
82
|
+
if (whole) return whole;
|
|
83
|
+
// Otherwise the digits really are split — a copied `123 456` — and joining
|
|
84
|
+
// them is the reconstruction that was always intended.
|
|
85
|
+
return runs.join("");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SignedIn {
|
|
89
|
+
id: string;
|
|
90
|
+
email: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether a sign-in flow actually ran.
|
|
95
|
+
*
|
|
96
|
+
* The caller uses it to decide whether its confirmation continues an open frame
|
|
97
|
+
* or stands on its own: the `│` spine means "a flow is happening", so printing
|
|
98
|
+
* one under a command that answered instantly from the session file would be a
|
|
99
|
+
* frame with no beginning.
|
|
100
|
+
*/
|
|
101
|
+
export interface EnsureSignedIn {
|
|
102
|
+
user: SignedIn;
|
|
103
|
+
prompted: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class LoginError extends Error {}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Whether this terminal can run an interactive prompt.
|
|
110
|
+
*
|
|
111
|
+
* Checked BEFORE anything is written, so a CI runner or a cron line gets one
|
|
112
|
+
* clear sentence instead of a hang on a `readline` nobody will ever answer.
|
|
113
|
+
*/
|
|
114
|
+
export function canPrompt(): boolean {
|
|
115
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The stored session, unless its refresh token has already expired.
|
|
120
|
+
*
|
|
121
|
+
* The check is a comparison against a number that is already in the file, so it
|
|
122
|
+
* keeps the offline-friendly property the doc below argues for: no request, no
|
|
123
|
+
* network failure mode, nothing new that a dropped wifi connection can break.
|
|
124
|
+
* What it removes is the case where every one of those is fine and the session
|
|
125
|
+
* is simply dead — revoked from another machine, or past its refresh window.
|
|
126
|
+
*
|
|
127
|
+
* Without it `--schedule` printed `reports to <email>` and exited 0 on a
|
|
128
|
+
* session that cannot mint another access token, so the user configured
|
|
129
|
+
* digests, was shown the destination, and then heard nothing for up to a full
|
|
130
|
+
* interval (90 days at the maximum) with the only signal a line in the journal.
|
|
131
|
+
* The dashboard already refuses this exact state in `setAutoAuditAction`, so
|
|
132
|
+
* the two surfaces disagreed on the one thing this feature claims is in sync.
|
|
133
|
+
*
|
|
134
|
+
* Expiry is treated as "sign in again", not as an error: the OTP prompt below
|
|
135
|
+
* is the remedy, and falling through to it is what makes this recoverable in
|
|
136
|
+
* one command instead of needing the file removed by hand.
|
|
137
|
+
*/
|
|
138
|
+
function sessionStillValid(auth: StoredAuth | null): StoredAuth | null {
|
|
139
|
+
if (!auth) return null;
|
|
140
|
+
// Seconds, per StoredAuth. A file whose value is missing was normalised to
|
|
141
|
+
// `access_expires_at` by `readAuth`, so this is always a real number.
|
|
142
|
+
return auth.refresh_expires_at * 1000 > Date.now() ? auth : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Return the current session, or run the OTP flow to create one.
|
|
147
|
+
*
|
|
148
|
+
* Deliberately NOT `whoAmI()`: that round-trips to `/v0/auth/me` and returns
|
|
149
|
+
* null on a network blip, which here would mean re-prompting a signed-in user
|
|
150
|
+
* for a code because their wifi dropped. The local file is the source of truth
|
|
151
|
+
* for "is somebody signed in on this machine"; whether the token still works is
|
|
152
|
+
* the reporting path's problem, and it already handles that by pausing digests
|
|
153
|
+
* rather than failing.
|
|
154
|
+
*/
|
|
155
|
+
export async function ensureSignedIn(preset?: string): Promise<EnsureSignedIn> {
|
|
156
|
+
const existing = sessionStillValid(readAuth());
|
|
157
|
+
if (existing) {
|
|
158
|
+
// A machine already reports as somebody. An `--email` naming a DIFFERENT
|
|
159
|
+
// address is refused rather than honoured: silently re-pointing where a
|
|
160
|
+
// machine's digests go is the kind of change nobody notices until they
|
|
161
|
+
// stop arriving, and the flag reads as "sign me in", not "switch accounts".
|
|
162
|
+
if (preset && !sameAddress(preset, existing.user.email)) {
|
|
163
|
+
throw new LoginError(
|
|
164
|
+
`This machine is already signed in as ${existing.user.email}.\n` +
|
|
165
|
+
`To report as ${preset.trim().toLowerCase()} instead, sign out first — ` +
|
|
166
|
+
`from the dashboard, or by removing ~/.failproofai/audit/session.json.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return { user: existing.user, prompted: false };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!canPrompt()) {
|
|
173
|
+
throw new LoginError(
|
|
174
|
+
"Signing in needs an interactive terminal, and this one is not.\n" +
|
|
175
|
+
"Run `failproofai audit --schedule` from a shell you are sitting at,\n" +
|
|
176
|
+
"or sign in from the dashboard — both write the same session file.",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { user: await runLogin(preset), prompted: true };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Addresses compare case-insensitively, because a mail server does. */
|
|
184
|
+
function sameAddress(a: string, b: string): boolean {
|
|
185
|
+
return a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Reject an address the flag supplied before anything is drawn or sent.
|
|
190
|
+
*
|
|
191
|
+
* Returned as a message rather than thrown so the caller can fail at the CLI
|
|
192
|
+
* boundary, next to where the day count is checked — a typo'd flag should look
|
|
193
|
+
* like a usage error, not like a sign-in that opened a frame and gave up.
|
|
194
|
+
*/
|
|
195
|
+
export function invalidEmail(address: string): string | null {
|
|
196
|
+
return EMAIL_RE.test(address.trim())
|
|
197
|
+
? null
|
|
198
|
+
: `\`--email\` needs an email address (got: ${address}).`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The two prompts, inside the frame `failproofai config` uses.
|
|
203
|
+
*
|
|
204
|
+
* Same logo, same `│` spine, same `◆ / ◇` step glyphs, same pink `└` close —
|
|
205
|
+
* because this is the same product asking, and a sign-in that looked like a
|
|
206
|
+
* different tool would be the one moment the seam showed. It is also the only
|
|
207
|
+
* moment this command asks for something personal, which is the moment worth
|
|
208
|
+
* spending the frame on.
|
|
209
|
+
*
|
|
210
|
+
* Exported so a test can drive it without the caller.
|
|
211
|
+
*/
|
|
212
|
+
export async function runLogin(preset?: string): Promise<SignedIn> {
|
|
213
|
+
intro("scheduled audits need somewhere to send the report");
|
|
214
|
+
|
|
215
|
+
let address: string;
|
|
216
|
+
if (preset) {
|
|
217
|
+
// Supplied on the command line, so the question is already answered — but
|
|
218
|
+
// it is still SHOWN, as a settled step, because it is the address a code is
|
|
219
|
+
// about to be sent to and the flag is exactly where a typo hides.
|
|
220
|
+
address = preset.trim().toLowerCase();
|
|
221
|
+
step("your email", address);
|
|
222
|
+
} else {
|
|
223
|
+
const email = await promptText({
|
|
224
|
+
prefix: spine(),
|
|
225
|
+
message: "your email",
|
|
226
|
+
hint: "you@yourdomain.com",
|
|
227
|
+
validate: (v) => (EMAIL_RE.test(v.trim()) ? null : "that doesn't look like an email"),
|
|
228
|
+
});
|
|
229
|
+
if (email === null) {
|
|
230
|
+
outro("Cancelled — nothing was changed.", { ok: false });
|
|
231
|
+
throw new LoginError("Cancelled.");
|
|
232
|
+
}
|
|
233
|
+
address = email.trim().toLowerCase();
|
|
234
|
+
}
|
|
235
|
+
let expiresInMin = 10;
|
|
236
|
+
try {
|
|
237
|
+
const sent = await requestLoginCode(address);
|
|
238
|
+
expiresInMin = Math.max(1, Math.ceil(sent.expires_in / 60));
|
|
239
|
+
} catch (err) {
|
|
240
|
+
outro("Could not send a login code.", { ok: false });
|
|
241
|
+
throw new LoginError(describeAuthError(err, "Could not send a login code"));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// The address is echoed back on the settled step rather than left to memory:
|
|
245
|
+
// a typo in it is the single most likely reason no code arrives, and this is
|
|
246
|
+
// the last place it can be noticed before somebody starts waiting.
|
|
247
|
+
step("code sent", `to ${address} · expires in ${expiresInMin} min`);
|
|
248
|
+
|
|
249
|
+
// Three attempts, matching the server's own per-code cap. Looping forever
|
|
250
|
+
// would keep a person typing at a code the server stopped accepting after
|
|
251
|
+
// the fifth try, and one attempt would punish a typo with a fresh email.
|
|
252
|
+
const ATTEMPTS = 3;
|
|
253
|
+
for (let attempt = 1; attempt <= ATTEMPTS; attempt += 1) {
|
|
254
|
+
stepOpen(attempt === 1 ? "the code from that email" : "try that code again");
|
|
255
|
+
const typed = await promptText({
|
|
256
|
+
prefix: spine(),
|
|
257
|
+
message: "code",
|
|
258
|
+
// Unmasked on purpose. A login code is single-use and expires in minutes,
|
|
259
|
+
// so hiding it protects nothing and costs the one thing that matters at
|
|
260
|
+
// this prompt: seeing your own typo before pressing enter.
|
|
261
|
+
hint:
|
|
262
|
+
attempt === 1
|
|
263
|
+
? "123456 · paste the whole line if you like"
|
|
264
|
+
: `attempt ${attempt} of ${ATTEMPTS}`,
|
|
265
|
+
validate: (v) => {
|
|
266
|
+
// No digits at all is not a mistyped code, it is not a code — caught
|
|
267
|
+
// here rather than spent as one of the server's five attempts.
|
|
268
|
+
if (!/\d/.test(v)) return "a code is digits — paste the line from the email";
|
|
269
|
+
const code = extractCode(v);
|
|
270
|
+
if (code.length < CODE_MIN) return "that looks too short to be the code";
|
|
271
|
+
if (code.length > CODE_MAX) return "that looks too long — paste just the code";
|
|
272
|
+
return null;
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
if (typed === null) {
|
|
276
|
+
outro("Cancelled — nothing was changed.", { ok: false });
|
|
277
|
+
throw new LoginError("Cancelled.");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const tokens = await verifyLoginCode(address, extractCode(typed));
|
|
282
|
+
writeAuth(authFromTokenResponse(tokens));
|
|
283
|
+
step("signed in", tokens.user.email);
|
|
284
|
+
return { id: tokens.user.id, email: tokens.user.email };
|
|
285
|
+
} catch (err) {
|
|
286
|
+
const wrongCode = err instanceof AuthApiError && err.code === "invalid_code";
|
|
287
|
+
if (wrongCode && attempt < ATTEMPTS) {
|
|
288
|
+
step(
|
|
289
|
+
"that code was wrong or expired",
|
|
290
|
+
`${ATTEMPTS - attempt} more ${ATTEMPTS - attempt === 1 ? "try" : "tries"} before it asks for a new one`,
|
|
291
|
+
);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
outro("Could not verify that code.", { ok: false });
|
|
295
|
+
throw new LoginError(describeAuthError(err, "Could not verify that code"));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
outro("Too many wrong codes.", { ok: false });
|
|
299
|
+
throw new LoginError("Too many wrong codes. Run the command again for a fresh one.");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* A sentence a person can act on.
|
|
304
|
+
*
|
|
305
|
+
* `AuthApiError` carries the server's own code, and two of them have a remedy
|
|
306
|
+
* worth naming rather than passing through: a rate limit is a wait, and an
|
|
307
|
+
* unreachable server on a machine pointed at localhost is almost always an
|
|
308
|
+
* api-server that is not running.
|
|
309
|
+
*/
|
|
310
|
+
function describeAuthError(err: unknown, prefix: string): string {
|
|
311
|
+
if (err instanceof AuthApiError) {
|
|
312
|
+
if (err.code === "rate_limited") {
|
|
313
|
+
const wait = err.retryAfterSecs;
|
|
314
|
+
return `${prefix}: too many attempts.${wait ? ` Try again in ${wait}s.` : ""}`;
|
|
315
|
+
}
|
|
316
|
+
if (err.status === 0 || err.code === "timeout") {
|
|
317
|
+
return (
|
|
318
|
+
`${prefix}: the api-server did not respond.\n` +
|
|
319
|
+
`It is at ${apiBaseForMessage()} — check that it is running,\n` +
|
|
320
|
+
`or set FAILPROOF_API_URL to point somewhere else.`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return `${prefix}: ${err.message}`;
|
|
324
|
+
}
|
|
325
|
+
return `${prefix}: ${err instanceof Error ? err.message : String(err)}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function apiBaseForMessage(): string {
|
|
329
|
+
return process.env.FAILPROOF_API_URL ?? process.env.FAILPROOFAI_API_URL ?? "https://api.befailproof.ai";
|
|
330
|
+
}
|
package/src/audit/cli.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { trackHookEvent } from "../hooks/hook-telemetry";
|
|
|
28
28
|
import { getInstanceId } from "../../lib/telemetry-id";
|
|
29
29
|
import { sanitizeErrorMessage } from "../../lib/telemetry-sanitize";
|
|
30
30
|
import { openWhenReady } from "./open-browser";
|
|
31
|
+
import { describeOutcome, reportHarm } from "./report-harm";
|
|
31
32
|
import { brandAnsi, ANSI_RESET, ANSI_BOLD, ANSI_DIM } from "../hooks/tui";
|
|
32
33
|
|
|
33
34
|
/** Port the bundled dashboard binds to. Matches `scripts/launch.ts`'s default
|
|
@@ -71,14 +72,34 @@ USAGE
|
|
|
71
72
|
server. This is what a scheduled audit runs; exit
|
|
72
73
|
75 means another audit already had the lock.
|
|
73
74
|
|
|
75
|
+
SCHEDULING
|
|
76
|
+
failproofai audit --schedule [days] [--email you@yourdomain.com]
|
|
77
|
+
Scan on a timer in the background (default 7 days,
|
|
78
|
+
1-90), and email you when a scan finds something
|
|
79
|
+
harmful. Signs you in the first time — the report
|
|
80
|
+
has to go somewhere. Pass --email to answer that
|
|
81
|
+
up front and go straight to entering the code.
|
|
82
|
+
failproofai audit --no-schedule
|
|
83
|
+
Stop scanning on a timer. Leaves you signed in.
|
|
84
|
+
failproofai audit --status
|
|
85
|
+
What this machine is doing: whether scheduling is
|
|
86
|
+
on, where reports go, the daemon's state, and when
|
|
87
|
+
the next scan is due.
|
|
88
|
+
|
|
89
|
+
These write the same ~/.failproofai/config.json the dashboard's settings page
|
|
90
|
+
writes, through the same function — so the two are always in step.
|
|
91
|
+
|
|
74
92
|
WHAT IT DOES
|
|
75
93
|
1. Scans past sessions from every installed agent CLI (Claude, Codex, Cursor,
|
|
76
94
|
Copilot, OpenCode, Pi) — entirely on your machine.
|
|
77
95
|
2. Starts the local dashboard and opens
|
|
78
96
|
http://localhost:${DASHBOARD_PORT}/audit with your results.
|
|
79
97
|
|
|
80
|
-
|
|
81
|
-
|
|
98
|
+
A bare "failproofai audit" needs no account, and nothing from your sessions
|
|
99
|
+
leaves this machine — anonymous usage counts still apply unless you set
|
|
100
|
+
FAILPROOFAI_TELEMETRY_DISABLED=1. Scheduling is the exception: it emails you
|
|
101
|
+
what it finds, so it needs an address.
|
|
102
|
+
Press Ctrl+C to stop the dashboard server when you're done.
|
|
82
103
|
`.trimStart();
|
|
83
104
|
|
|
84
105
|
// ── ANSI helpers ────────────────────────────────────────────────────────────
|
|
@@ -365,6 +386,29 @@ export async function runScheduledAudit(): Promise<number> {
|
|
|
365
386
|
`${num(result.transcripts.scanned)} sessions, ${num(result.totals.hits)} hits\n`,
|
|
366
387
|
);
|
|
367
388
|
|
|
389
|
+
// Report harmful findings upstream, if the user switched emailed reports on.
|
|
390
|
+
//
|
|
391
|
+
// AFTER the dashboard cache is written and AFTER the success line, because
|
|
392
|
+
// the scan is the product and this is an optional extra on top of it.
|
|
393
|
+
// `reportHarm` never throws — every failure inside it is an outcome — so a
|
|
394
|
+
// dead network, an expired session or an api-server having a bad day cannot
|
|
395
|
+
// turn a successful scan into exit 1. A machine that never opted in prints
|
|
396
|
+
// nothing at all and does no work here.
|
|
397
|
+
//
|
|
398
|
+
// Scheduled runs ONLY. An interactive `failproofai audit` has a person
|
|
399
|
+
// sitting in front of the result, so mailing it to them is noise, and it
|
|
400
|
+
// would also make the manual command do a network call that
|
|
401
|
+
// `audit --help` promises it does not.
|
|
402
|
+
const outcome = await reportHarm(result);
|
|
403
|
+
const line = describeOutcome(outcome);
|
|
404
|
+
if (line) {
|
|
405
|
+
// Anything other than a successful send goes to stderr: on a scheduled run
|
|
406
|
+
// the journal is the only reader, and "the email did not go out" is the
|
|
407
|
+
// half worth finding with a grep.
|
|
408
|
+
const stream = outcome.kind === "sent" ? process.stdout : process.stderr;
|
|
409
|
+
stream.write(`${line}\n`);
|
|
410
|
+
}
|
|
411
|
+
|
|
368
412
|
return 0;
|
|
369
413
|
} finally {
|
|
370
414
|
attempt.lock.release();
|
|
@@ -460,19 +504,93 @@ export async function runAuditCli(args: string[]): Promise<void> {
|
|
|
460
504
|
// The headless path, spawned rather than typed. Handled ahead of the
|
|
461
505
|
// rejection below so that adding it costs the interactive path nothing: it
|
|
462
506
|
// still refuses every argument it has always refused.
|
|
507
|
+
//
|
|
508
|
+
// `--scheduled` (run one now, headlessly) and `--schedule` (put runs on a
|
|
509
|
+
// timer) differ by one letter and do completely different things, so this
|
|
510
|
+
// is checked FIRST and exactly: a `--schedule` typo must not silently start
|
|
511
|
+
// a 100-second scan, and `--scheduled` must never be read as configuration.
|
|
463
512
|
if (args.includes("--scheduled")) {
|
|
464
513
|
const extra = args.find((a) => a !== "--scheduled");
|
|
465
514
|
if (extra) die(`\`audit --scheduled\` takes no other arguments (got: ${extra}).`);
|
|
466
515
|
process.exit(await runScheduledAudit());
|
|
467
516
|
}
|
|
468
517
|
|
|
469
|
-
//
|
|
470
|
-
|
|
518
|
+
// The scheduling controls. These write config and exit; none of them scan.
|
|
519
|
+
if (args.includes("--status")) {
|
|
520
|
+
const extra = args.find((a) => a !== "--status");
|
|
521
|
+
if (extra) die(`\`audit --status\` takes no other arguments (got: ${extra}).`);
|
|
522
|
+
const { runScheduleStatus } = await import("./schedule-cli");
|
|
523
|
+
runScheduleStatus();
|
|
524
|
+
process.exit(0);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (args.includes("--no-schedule")) {
|
|
528
|
+
const extra = args.find((a) => a !== "--no-schedule");
|
|
529
|
+
if (extra) die(`\`audit --no-schedule\` takes no other arguments (got: ${extra}).`);
|
|
530
|
+
const { runScheduleOff } = await import("./schedule-cli");
|
|
531
|
+
runScheduleOff();
|
|
532
|
+
process.exit(0);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const scheduleAt = args.indexOf("--schedule");
|
|
536
|
+
if (scheduleAt !== -1) {
|
|
537
|
+
// Parsed POSITIONALLY rather than by matching values against a set: an
|
|
538
|
+
// address and a day count are both just strings, and "have I already seen
|
|
539
|
+
// this string" cannot tell the argument of one flag from the argument of
|
|
540
|
+
// another.
|
|
541
|
+
let days: string | undefined;
|
|
542
|
+
let email: string | undefined;
|
|
543
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
544
|
+
const a = args[i];
|
|
545
|
+
if (a === "--schedule") {
|
|
546
|
+
// The day count is OPTIONAL, so the next token counts only when it is
|
|
547
|
+
// not itself a flag — `--schedule --email x` must not read "--email"
|
|
548
|
+
// as a number of days.
|
|
549
|
+
const next = args[i + 1];
|
|
550
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
551
|
+
days = next;
|
|
552
|
+
i += 1;
|
|
553
|
+
}
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (a === "--email" || a.startsWith("--email=")) {
|
|
557
|
+
// Both forms, because both are what people type.
|
|
558
|
+
if (a.startsWith("--email=")) {
|
|
559
|
+
email = a.slice("--email=".length);
|
|
560
|
+
} else {
|
|
561
|
+
email = args[i + 1];
|
|
562
|
+
i += 1;
|
|
563
|
+
}
|
|
564
|
+
if (email === undefined || email.length === 0 || email.startsWith("-")) {
|
|
565
|
+
die("`--email` needs an address, e.g. `--email you@yourdomain.com`.");
|
|
566
|
+
}
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
die(`\`audit --schedule\` does not take ${a}.`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const { runScheduleOn, ScheduleCliError } = await import("./schedule-cli");
|
|
573
|
+
const { LoginError } = await import("./cli-login");
|
|
574
|
+
try {
|
|
575
|
+
await runScheduleOn(days, email);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
// Both are "the user needs to read one sentence and try again", not a
|
|
578
|
+
// stack trace: a wrong day count, a cancelled prompt, an api-server that
|
|
579
|
+
// is not running.
|
|
580
|
+
if (err instanceof ScheduleCliError || err instanceof LoginError) die(err.message);
|
|
581
|
+
throw err;
|
|
582
|
+
}
|
|
583
|
+
process.exit(0);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Anything else is rejected rather than silently doing a bare audit, so a
|
|
587
|
+
// typo like `--sched` does not quietly scan and exit 0 looking like it worked.
|
|
471
588
|
const stray = args.find((a) => a !== "--help" && a !== "-h");
|
|
472
589
|
if (stray) {
|
|
473
590
|
die(
|
|
474
|
-
`\`audit\`
|
|
475
|
-
`Run \`failproofai audit\` to scan your history and open the dashboard
|
|
591
|
+
`\`audit\` does not take ${stray}.\n` +
|
|
592
|
+
`Run \`failproofai audit\` to scan your history and open the dashboard,\n` +
|
|
593
|
+
`or \`failproofai audit --help\` for the scheduling commands.`,
|
|
476
594
|
);
|
|
477
595
|
}
|
|
478
596
|
|
|
@@ -126,14 +126,38 @@ export function isCacheStale(cachedAt: string, maxAgeMinutes: number = DEFAULT_M
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* "no audit has ever run" from
|
|
132
|
-
* return whose age exceeds
|
|
133
|
-
*
|
|
134
|
-
* missing or schema-incompatible
|
|
129
|
+
* What the last audit on this machine found, **bypassing** the TTL check.
|
|
130
|
+
*
|
|
131
|
+
* Used by the empty-state path to tell apart "no audit has ever run" from
|
|
132
|
+
* "your last audit aged out" — a non-null return whose age exceeds
|
|
133
|
+
* `DASHBOARD_CACHE_TTL_MINUTES` means `readDashboardCache()` rejected it for
|
|
134
|
+
* being expired rather than missing or schema-incompatible — and by /settings
|
|
135
|
+
* to draw its LAST SCAN and FINDINGS stats.
|
|
136
|
+
*
|
|
137
|
+
* Those stats deliberately read from HERE and not from `readDashboardCache()`.
|
|
138
|
+
* That reader drops an entry once it passes the TTL, which is right for a
|
|
139
|
+
* dashboard rendering results and exactly backwards for a stat whose subject is
|
|
140
|
+
* how long ago the scan was: a page that answers "when did this last run" must
|
|
141
|
+
* not lose the answer for the crime of it having been a while. Reading the
|
|
142
|
+
* timestamp from one function and the counts from the other is the specific bug
|
|
143
|
+
* this shape prevents — "6 days ago" beside a blank findings cell, because the
|
|
144
|
+
* two disagreed about whether the same file existed.
|
|
145
|
+
*
|
|
146
|
+
* The counts come from the same object the /audit page renders, so they cannot
|
|
147
|
+
* drift from it: `totals.hits` is the headline finding count and
|
|
148
|
+
* `transcripts.scanned` the sessions behind it.
|
|
135
149
|
*/
|
|
136
|
-
export
|
|
150
|
+
export interface DashboardCacheSummary {
|
|
151
|
+
cachedAt: string;
|
|
152
|
+
/** `AuditResult.totals.hits` — total policy hits in that scan. */
|
|
153
|
+
findings: number | null;
|
|
154
|
+
/** `AuditResult.transcripts.scanned` — session transcripts walked. */
|
|
155
|
+
sessionsScanned: number | null;
|
|
156
|
+
/** `AuditResult.eventsScanned` — normalized tool-use events walked. */
|
|
157
|
+
eventsScanned: number | null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function readDashboardCacheMeta(): DashboardCacheSummary | null {
|
|
137
161
|
const cachePath = getCachePath();
|
|
138
162
|
if (!existsSync(cachePath)) return null;
|
|
139
163
|
try {
|
|
@@ -150,7 +174,19 @@ export function readDashboardCacheMeta(): { cachedAt: string } | null {
|
|
|
150
174
|
|| typeof entry.cachedAt !== "string"
|
|
151
175
|
|| Number.isNaN(new Date(entry.cachedAt).getTime())
|
|
152
176
|
) return null;
|
|
153
|
-
|
|
177
|
+
// The counts are read defensively rather than assumed: `schemaVersion`
|
|
178
|
+
// guards the ENTRY's shape, and a torn or hand-edited `result` can still
|
|
179
|
+
// arrive with a missing field. A null count renders as "—" (unknown), which
|
|
180
|
+
// is a different claim from 0 (scanned, found nothing) — collapsing the two
|
|
181
|
+
// would report a clean machine on a file we failed to read.
|
|
182
|
+
const result = entry.result;
|
|
183
|
+
const count = (v: unknown): number | null => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
184
|
+
return {
|
|
185
|
+
cachedAt: entry.cachedAt,
|
|
186
|
+
findings: count(result?.totals?.hits),
|
|
187
|
+
sessionsScanned: count(result?.transcripts?.scanned),
|
|
188
|
+
eventsScanned: count(result?.eventsScanned),
|
|
189
|
+
};
|
|
154
190
|
} catch {
|
|
155
191
|
return null;
|
|
156
192
|
}
|