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,354 @@
|
|
|
1
|
+
/* /settings — a console for the service running on this machine.
|
|
2
|
+
*
|
|
3
|
+
* Every colour is an existing token from globals.css. No new hue, no third
|
|
4
|
+
* accent: pink is the control that acts and anything that needs a person, mint
|
|
5
|
+
* is the thing that is alive, everything else is the charcoal stack and the ink
|
|
6
|
+
* ramp. The chunky pixel shadow comes from `.btn-press`, which already exists —
|
|
7
|
+
* this file puts one on the console and nothing else.
|
|
8
|
+
*
|
|
9
|
+
* ## The hairline grid
|
|
10
|
+
*
|
|
11
|
+
* Every rule between cells is a `gap: 1px` over a line-coloured background, not
|
|
12
|
+
* a border on each cell. Borders double where two cells meet and go missing at
|
|
13
|
+
* the edges; a gap over a background cannot do either, so the grid is exactly
|
|
14
|
+
* one pixel everywhere by construction rather than by arithmetic.
|
|
15
|
+
*
|
|
16
|
+
* The tape is the only drawn element on the page. That is the budget.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
.set-sec {
|
|
20
|
+
max-width: 940px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ── the console: stat row over panel row, one hairline grid ──────────────── */
|
|
24
|
+
|
|
25
|
+
.set-console {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
/* The gap IS the grid — see the note above. */
|
|
29
|
+
gap: 1px;
|
|
30
|
+
background: var(--line);
|
|
31
|
+
border: 1px solid var(--line);
|
|
32
|
+
/* Print-register depth, matching `.btn-press`. Hard offset, never a blur. */
|
|
33
|
+
box-shadow: 6px 6px 0 0 var(--accent-pink-shadow);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.set-cell {
|
|
37
|
+
background: var(--bg-2);
|
|
38
|
+
padding: 20px 22px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.set-stats {
|
|
42
|
+
display: grid;
|
|
43
|
+
grid-template-columns: repeat(4, 1fr);
|
|
44
|
+
gap: 1px;
|
|
45
|
+
background: var(--line);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.set-stat {
|
|
49
|
+
background: var(--bg-2);
|
|
50
|
+
padding: 16px 18px 15px;
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
gap: 7px;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
}
|
|
56
|
+
.set-stat-label {
|
|
57
|
+
font-family: var(--font-mono);
|
|
58
|
+
font-size: 9.5px;
|
|
59
|
+
font-weight: 500;
|
|
60
|
+
letter-spacing: 0.18em;
|
|
61
|
+
text-transform: uppercase;
|
|
62
|
+
color: var(--dim);
|
|
63
|
+
}
|
|
64
|
+
.set-stat-value {
|
|
65
|
+
/* The pixel display face, on the page title and these four numbers only. */
|
|
66
|
+
font-family: var(--font-display);
|
|
67
|
+
font-size: 26px;
|
|
68
|
+
line-height: 1;
|
|
69
|
+
letter-spacing: 0.04em;
|
|
70
|
+
color: var(--ink);
|
|
71
|
+
text-transform: lowercase;
|
|
72
|
+
/* Four cells of digits should line up as a row, not drift per glyph width. */
|
|
73
|
+
font-variant-numeric: tabular-nums;
|
|
74
|
+
overflow-wrap: anywhere;
|
|
75
|
+
}
|
|
76
|
+
.set-stat-value.ok { color: var(--accent-green); }
|
|
77
|
+
.set-stat-value.warn { color: var(--accent-pink); }
|
|
78
|
+
.set-stat-sub {
|
|
79
|
+
font-family: var(--font-mono);
|
|
80
|
+
font-size: 10.5px;
|
|
81
|
+
line-height: 1.5;
|
|
82
|
+
color: var(--ink-2);
|
|
83
|
+
/* Reserve the line even when empty, so cells with and without a sub-line
|
|
84
|
+
* keep the same height and the row's baselines stay level. */
|
|
85
|
+
min-height: 1.5em;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* ── panel row: controls, then what the scan does ─────────────────────────── */
|
|
89
|
+
|
|
90
|
+
.set-cols {
|
|
91
|
+
display: grid;
|
|
92
|
+
/* ~56/44 — the controls carry more, so they get more. */
|
|
93
|
+
grid-template-columns: 1.28fr 1fr;
|
|
94
|
+
gap: 1px;
|
|
95
|
+
background: var(--line);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.set-config {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 16px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.set-cell-title {
|
|
105
|
+
font-family: var(--font-mono);
|
|
106
|
+
font-size: 10px;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
letter-spacing: 0.18em;
|
|
109
|
+
text-transform: uppercase;
|
|
110
|
+
color: var(--ink-2);
|
|
111
|
+
margin: 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.set-actions {
|
|
115
|
+
margin-top: auto;
|
|
116
|
+
padding-top: 4px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ── how it works ─────────────────────────────────────────────────────────── */
|
|
120
|
+
|
|
121
|
+
.set-how {
|
|
122
|
+
display: flex;
|
|
123
|
+
flex-direction: column;
|
|
124
|
+
gap: 16px;
|
|
125
|
+
}
|
|
126
|
+
.set-how-list {
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
gap: 14px;
|
|
130
|
+
margin: 0;
|
|
131
|
+
}
|
|
132
|
+
.set-how-row {
|
|
133
|
+
display: grid;
|
|
134
|
+
grid-template-columns: 58px 1fr;
|
|
135
|
+
gap: 12px;
|
|
136
|
+
align-items: baseline;
|
|
137
|
+
}
|
|
138
|
+
.set-how-label {
|
|
139
|
+
font-family: var(--font-mono);
|
|
140
|
+
font-size: 9.5px;
|
|
141
|
+
font-weight: 500;
|
|
142
|
+
letter-spacing: 0.16em;
|
|
143
|
+
text-transform: uppercase;
|
|
144
|
+
color: var(--accent-pink);
|
|
145
|
+
}
|
|
146
|
+
.set-how-body {
|
|
147
|
+
font-family: var(--font-mono);
|
|
148
|
+
font-size: 11.5px;
|
|
149
|
+
line-height: 1.65;
|
|
150
|
+
color: var(--ink-2);
|
|
151
|
+
margin: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ── masthead ─────────────────────────────────────────────────────────────── */
|
|
155
|
+
|
|
156
|
+
.set-mast {
|
|
157
|
+
margin-bottom: 28px;
|
|
158
|
+
}
|
|
159
|
+
.set-title {
|
|
160
|
+
/* The pixel display face, used once on the page and nowhere else. */
|
|
161
|
+
font-family: var(--font-display);
|
|
162
|
+
font-size: 40px;
|
|
163
|
+
line-height: 1;
|
|
164
|
+
letter-spacing: 0.06em;
|
|
165
|
+
color: var(--ink);
|
|
166
|
+
margin: 0 0 10px;
|
|
167
|
+
text-transform: lowercase;
|
|
168
|
+
}
|
|
169
|
+
.set-lede {
|
|
170
|
+
font-family: var(--font-mono);
|
|
171
|
+
font-size: 13px;
|
|
172
|
+
line-height: 1.6;
|
|
173
|
+
color: var(--ink-2);
|
|
174
|
+
margin: 0;
|
|
175
|
+
/* Wide enough to hold the sentence on one line at desktop width — 52ch broke
|
|
176
|
+
* it after "not", which reads as a typo rather than a line. */
|
|
177
|
+
max-width: 64ch;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* ── the panel ────────────────────────────────────────────────────────────── */
|
|
181
|
+
|
|
182
|
+
/* ── rows ─────────────────────────────────────────────────────────────────── */
|
|
183
|
+
|
|
184
|
+
.set-row {
|
|
185
|
+
display: flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
gap: 12px;
|
|
188
|
+
font-family: var(--font-mono);
|
|
189
|
+
font-size: 13px;
|
|
190
|
+
color: var(--ink);
|
|
191
|
+
}
|
|
192
|
+
.set-row-main {
|
|
193
|
+
align-items: flex-start;
|
|
194
|
+
}
|
|
195
|
+
.set-row-copy {
|
|
196
|
+
display: flex;
|
|
197
|
+
flex-direction: column;
|
|
198
|
+
gap: 3px;
|
|
199
|
+
line-height: 1.5;
|
|
200
|
+
}
|
|
201
|
+
.set-row-interval {
|
|
202
|
+
gap: 10px;
|
|
203
|
+
flex-wrap: wrap;
|
|
204
|
+
}
|
|
205
|
+
.set-row-identity {
|
|
206
|
+
flex-wrap: wrap;
|
|
207
|
+
gap: 8px;
|
|
208
|
+
font-size: 12px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.set-strong { color: var(--ink); }
|
|
212
|
+
.set-dim { color: var(--ink-2); }
|
|
213
|
+
.set-hint { color: var(--dim); font-size: 11px; }
|
|
214
|
+
.set-email { color: var(--accent-green); }
|
|
215
|
+
|
|
216
|
+
.set-num {
|
|
217
|
+
font-family: var(--font-mono);
|
|
218
|
+
font-size: 13px;
|
|
219
|
+
width: 62px;
|
|
220
|
+
padding: 6px 10px;
|
|
221
|
+
background: var(--bg);
|
|
222
|
+
border: 1px solid var(--line-2);
|
|
223
|
+
color: var(--ink);
|
|
224
|
+
border-radius: 0;
|
|
225
|
+
}
|
|
226
|
+
.set-num:focus-visible {
|
|
227
|
+
outline: 2px solid var(--accent-pink);
|
|
228
|
+
outline-offset: 1px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.set-link {
|
|
232
|
+
font-family: var(--font-mono);
|
|
233
|
+
font-size: 12px;
|
|
234
|
+
color: var(--dim);
|
|
235
|
+
background: none;
|
|
236
|
+
border: none;
|
|
237
|
+
padding: 0;
|
|
238
|
+
text-decoration: underline;
|
|
239
|
+
text-underline-offset: 3px;
|
|
240
|
+
}
|
|
241
|
+
.set-link:hover:not(:disabled) { color: var(--ink); }
|
|
242
|
+
.set-link:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
243
|
+
.set-link:focus-visible { outline: 2px solid var(--accent-pink); outline-offset: 2px; }
|
|
244
|
+
|
|
245
|
+
.set-warn {
|
|
246
|
+
font-family: var(--font-mono);
|
|
247
|
+
font-size: 12px;
|
|
248
|
+
line-height: 1.6;
|
|
249
|
+
color: var(--accent-pink);
|
|
250
|
+
margin: 0;
|
|
251
|
+
}
|
|
252
|
+
.set-warn-inline { color: var(--accent-pink); }
|
|
253
|
+
|
|
254
|
+
.set-rule {
|
|
255
|
+
height: 1px;
|
|
256
|
+
background: var(--line);
|
|
257
|
+
margin: 2px 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* ── the switch ───────────────────────────────────────────────────────────── */
|
|
261
|
+
|
|
262
|
+
.sw {
|
|
263
|
+
position: relative;
|
|
264
|
+
flex: none;
|
|
265
|
+
width: 38px;
|
|
266
|
+
height: 20px;
|
|
267
|
+
border-radius: 10px;
|
|
268
|
+
border: none;
|
|
269
|
+
background: var(--line-2);
|
|
270
|
+
cursor: pointer;
|
|
271
|
+
padding: 0;
|
|
272
|
+
transition: background-color 140ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
273
|
+
}
|
|
274
|
+
.sw[data-on="true"] { background: var(--accent-pink); }
|
|
275
|
+
.sw:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
276
|
+
.sw:focus-visible { outline: 2px solid var(--accent-pink); outline-offset: 3px; }
|
|
277
|
+
.sw-knob {
|
|
278
|
+
position: absolute;
|
|
279
|
+
top: 3px;
|
|
280
|
+
left: 3px;
|
|
281
|
+
width: 14px;
|
|
282
|
+
height: 14px;
|
|
283
|
+
border-radius: 50%;
|
|
284
|
+
background: var(--bg);
|
|
285
|
+
transition: transform 140ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
286
|
+
}
|
|
287
|
+
.sw[data-on="true"] .sw-knob { transform: translateX(18px); }
|
|
288
|
+
|
|
289
|
+
/* ── the schedule tape — the one drawn element ────────────────────────────── */
|
|
290
|
+
|
|
291
|
+
.tape {
|
|
292
|
+
display: flex;
|
|
293
|
+
flex-direction: column;
|
|
294
|
+
gap: 8px;
|
|
295
|
+
/* Clears the console's 6px pixel shadow rather than sitting on it. */
|
|
296
|
+
margin-top: 26px;
|
|
297
|
+
}
|
|
298
|
+
.tape-rail {
|
|
299
|
+
position: relative;
|
|
300
|
+
height: 3px;
|
|
301
|
+
background: var(--line-2);
|
|
302
|
+
}
|
|
303
|
+
.tape-fill {
|
|
304
|
+
position: absolute;
|
|
305
|
+
inset: 0 auto 0 0;
|
|
306
|
+
background: var(--accent-green);
|
|
307
|
+
opacity: 0.55;
|
|
308
|
+
}
|
|
309
|
+
.tape-now {
|
|
310
|
+
position: absolute;
|
|
311
|
+
top: -4px;
|
|
312
|
+
width: 2px;
|
|
313
|
+
height: 11px;
|
|
314
|
+
background: var(--accent-pink);
|
|
315
|
+
/* Centre the marker on its position rather than hanging it to the right. */
|
|
316
|
+
transform: translateX(-1px);
|
|
317
|
+
}
|
|
318
|
+
.tape-ends {
|
|
319
|
+
display: flex;
|
|
320
|
+
justify-content: space-between;
|
|
321
|
+
gap: 12px;
|
|
322
|
+
font-family: var(--font-mono);
|
|
323
|
+
font-size: 10.5px;
|
|
324
|
+
letter-spacing: 0.04em;
|
|
325
|
+
color: var(--dim);
|
|
326
|
+
}
|
|
327
|
+
.tape-next { color: var(--ink-2); }
|
|
328
|
+
|
|
329
|
+
/* ── narrow ───────────────────────────────────────────────────────────────── */
|
|
330
|
+
|
|
331
|
+
/* The stat row folds 4 → 2 before it folds 2 → 1: four cells at phone width
|
|
332
|
+
* would each be too narrow for a display-face number, and a single column
|
|
333
|
+
* makes four facts into a list you scroll rather than a row you scan. */
|
|
334
|
+
@media (max-width: 860px) {
|
|
335
|
+
.set-stats { grid-template-columns: repeat(2, 1fr); }
|
|
336
|
+
.set-cols { grid-template-columns: 1fr; }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@media (max-width: 640px) {
|
|
340
|
+
.set-title { font-size: 32px; }
|
|
341
|
+
.set-console { box-shadow: 4px 4px 0 0 var(--accent-pink-shadow); }
|
|
342
|
+
.set-stat-value { font-size: 22px; }
|
|
343
|
+
.set-cell { padding: 18px; }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
@media (max-width: 420px) {
|
|
347
|
+
.set-stats { grid-template-columns: 1fr; }
|
|
348
|
+
.set-how-row { grid-template-columns: 1fr; gap: 4px; }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
@media (prefers-reduced-motion: reduce) {
|
|
352
|
+
.sw,
|
|
353
|
+
.sw-knob { transition: none; }
|
|
354
|
+
}
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
/** Top navigation bar — wordmark, primary nav, refresh + reach-
|
|
1
|
+
/** Top navigation bar — wordmark, primary nav, refresh + settings + reach-us.
|
|
2
2
|
*
|
|
3
3
|
* Restyled to the audit / brutalist-pixel-craft system: the wordmark uses the
|
|
4
4
|
* same pixel pink mark + Bitcount Prop Single lowercase name as the audit
|
|
5
5
|
* report, and each nav link is a `.tab` with a sharp pink underline on the
|
|
6
|
-
* active route.
|
|
7
|
-
*
|
|
6
|
+
* active route.
|
|
7
|
+
*
|
|
8
|
+
* The bar was text-forward with no icons. Settings is the one exception, and
|
|
9
|
+
* deliberately so: the tabs are VIEWS OF DATA (projects, policies, audit) and
|
|
10
|
+
* settings is machine configuration, so putting it in that row would have
|
|
11
|
+
* claimed it was another place to look at results. As an icon among the other
|
|
12
|
+
* chrome controls it reads as what it is.
|
|
8
13
|
*/
|
|
9
14
|
"use client";
|
|
10
15
|
|
|
11
16
|
import React from "react";
|
|
12
17
|
import Link from "next/link";
|
|
13
18
|
import { usePathname } from "next/navigation";
|
|
19
|
+
import { Settings } from "lucide-react";
|
|
14
20
|
import { ReachDevelopers } from "@/components/reach-developers";
|
|
15
21
|
import { RefreshButton } from "@/app/components/refresh-button";
|
|
16
22
|
import { usePostHog } from "@/contexts/PostHogContext";
|
|
@@ -19,7 +25,6 @@ const NAV_LINKS = [
|
|
|
19
25
|
{ href: "/projects", label: "projects" },
|
|
20
26
|
{ href: "/policies", label: "policies" },
|
|
21
27
|
{ href: "/audit", label: "audit" },
|
|
22
|
-
{ href: "/settings", label: "settings" },
|
|
23
28
|
];
|
|
24
29
|
|
|
25
30
|
const REMOTE_LOGO_URL =
|
|
@@ -131,6 +136,30 @@ export const Navbar: React.FC<{
|
|
|
131
136
|
<div className="h-actions">
|
|
132
137
|
<RefreshButton />
|
|
133
138
|
<span style={{ width: 1, height: 18, background: "var(--line)", margin: "0 6px" }} />
|
|
139
|
+
{/* Settings sits between the refresh controls and reach-us as an ICON,
|
|
140
|
+
not a nav tab: it is machine configuration rather than a view of the
|
|
141
|
+
data, so it belongs with the other chrome controls rather than
|
|
142
|
+
beside projects / policies / audit.
|
|
143
|
+
|
|
144
|
+
Being chrome rather than a tab is what put it OUTSIDE the filter
|
|
145
|
+
above, so `FAILPROOFAI_DISABLE_PAGES=settings` hid nothing and the
|
|
146
|
+
gear kept pointing at a page the operator had turned off. The tabs
|
|
147
|
+
and the icon are different placements of the same idea, and the
|
|
148
|
+
filter applies to both. */}
|
|
149
|
+
{!disabledPages.includes("settings") && (
|
|
150
|
+
<>
|
|
151
|
+
<Link
|
|
152
|
+
href="/settings"
|
|
153
|
+
className={`h-icon-btn${pathname.startsWith("/settings") ? " is-active" : ""}`}
|
|
154
|
+
aria-label="settings"
|
|
155
|
+
aria-current={pathname.startsWith("/settings") ? "page" : undefined}
|
|
156
|
+
title="settings"
|
|
157
|
+
>
|
|
158
|
+
<Settings className="w-3.5 h-3.5" strokeWidth={1.75} aria-hidden="true" />
|
|
159
|
+
</Link>
|
|
160
|
+
<span style={{ width: 1, height: 18, background: "var(--line)", margin: "0 6px" }} />
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
134
163
|
<ReachDevelopers />
|
|
135
164
|
</div>
|
|
136
165
|
</header>
|
|
@@ -107,7 +107,7 @@ async function parseError(res: Response): Promise<AuthApiError> {
|
|
|
107
107
|
return new AuthApiError(res.status, code, message, retryAfterSecs);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
/** Hard cap on every auth/
|
|
110
|
+
/** Hard cap on every auth/report HTTP call. Without this, a wedged DNS
|
|
111
111
|
* resolver or a hung server keeps the CLI / dashboard route stuck forever. */
|
|
112
112
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
113
113
|
|
|
@@ -210,34 +210,6 @@ export async function fetchMe(accessToken: string): Promise<MeResponse> {
|
|
|
210
210
|
return getJson<MeResponse>("/v0/auth/me", accessToken);
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
export interface ServerReminder {
|
|
214
|
-
user_id: string;
|
|
215
|
-
email: string;
|
|
216
|
-
fire_at: number; // unix seconds
|
|
217
|
-
set_at: number; // unix seconds
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export async function scheduleReminder(
|
|
221
|
-
accessToken: string,
|
|
222
|
-
body: { in_days?: number; at?: number },
|
|
223
|
-
): Promise<ServerReminder> {
|
|
224
|
-
const res = await postJson<{ reminder: ServerReminder }>(
|
|
225
|
-
"/v0/reminders",
|
|
226
|
-
body,
|
|
227
|
-
{ accessToken },
|
|
228
|
-
);
|
|
229
|
-
return res.reminder;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export async function cancelReminder(accessToken: string): Promise<void> {
|
|
233
|
-
const res = await fetchWithTimeout(`${getApiBase()}/v0/reminders`, {
|
|
234
|
-
method: "DELETE",
|
|
235
|
-
headers: { authorization: `Bearer ${accessToken}` },
|
|
236
|
-
});
|
|
237
|
-
if (res.status === 204 || res.ok) return;
|
|
238
|
-
throw await parseError(res);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
213
|
export interface InviteSendResult {
|
|
242
214
|
/** Recipients that were dispatched successfully. */
|
|
243
215
|
sent: string[];
|
|
@@ -266,6 +238,56 @@ export async function sendInvites(
|
|
|
266
238
|
);
|
|
267
239
|
}
|
|
268
240
|
|
|
241
|
+
export interface AuditReportBody {
|
|
242
|
+
machine_id: string;
|
|
243
|
+
label?: string;
|
|
244
|
+
platform?: string;
|
|
245
|
+
window_from?: string;
|
|
246
|
+
window_to: string;
|
|
247
|
+
harmful: {
|
|
248
|
+
policy: string;
|
|
249
|
+
category: string;
|
|
250
|
+
title: string;
|
|
251
|
+
hits: number;
|
|
252
|
+
first_seen?: string;
|
|
253
|
+
last_seen?: string;
|
|
254
|
+
examples: string[];
|
|
255
|
+
}[];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface AuditReportResult {
|
|
259
|
+
report_id: string;
|
|
260
|
+
/** Whether this report produced an email. */
|
|
261
|
+
emailed: boolean;
|
|
262
|
+
/** `below_threshold`, `cooldown`, `send_failed`, or null when mail went out. */
|
|
263
|
+
reason: string | null;
|
|
264
|
+
/**
|
|
265
|
+
* Where the next window starts, per the SERVER.
|
|
266
|
+
*
|
|
267
|
+
* Persisted verbatim rather than computed locally. The server anchors it on
|
|
268
|
+
* the last DELIVERED digest, so a report held by the cooldown — or one whose
|
|
269
|
+
* send failed — correctly leaves the watermark where it was, and its findings
|
|
270
|
+
* turn up in the next digest instead of falling into a gap. A machine that
|
|
271
|
+
* lost `machine.json` also resyncs here rather than re-reporting from the
|
|
272
|
+
* beginning of time.
|
|
273
|
+
*/
|
|
274
|
+
next_window_from: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Submit one scheduled scan's harmful findings.
|
|
279
|
+
*
|
|
280
|
+
* Called only by the audit child, and only on `--scheduled`. The destination
|
|
281
|
+
* address is never sent: the api-server takes it from the access-token claims,
|
|
282
|
+
* so a report cannot name where its digest goes.
|
|
283
|
+
*/
|
|
284
|
+
export async function submitAuditReport(
|
|
285
|
+
accessToken: string,
|
|
286
|
+
body: AuditReportBody,
|
|
287
|
+
): Promise<AuditReportResult> {
|
|
288
|
+
return postJson<AuditReportResult>("/v0/audit-reports", body, { accessToken });
|
|
289
|
+
}
|
|
290
|
+
|
|
269
291
|
interface JwtClaims {
|
|
270
292
|
sub: string;
|
|
271
293
|
email: string;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Persistence layer for the
|
|
2
|
+
* Persistence layer for the signed-in session.
|
|
3
3
|
*
|
|
4
|
-
* Tokens live at
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Tokens live at `~/.failproofai/audit/session.json` with mode 0600 (layout 4;
|
|
5
|
+
* `auth.json` at the home root before that). The dashboard's Next.js API routes
|
|
6
|
+
* and the audit child both read and write through here, so a session survives
|
|
7
|
+
* across dashboard runs and is the same one a scheduled report uses.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
11
12
|
|
|
12
13
|
import { writeJsonAtomically } from "../atomic-write";
|
|
13
|
-
import {
|
|
14
|
+
import { auditDir, auditSessionFile, migrationsDir } from "../../src/hooks/fp-home";
|
|
14
15
|
import {
|
|
15
16
|
AuthApiError,
|
|
16
17
|
decodeJwt,
|
|
@@ -27,62 +28,24 @@ export interface StoredAuth {
|
|
|
27
28
|
user: { id: string; email: string };
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Where the session file lives.
|
|
33
|
+
*
|
|
34
|
+
* `FAILPROOFAI_AUTH_DIR` overrides it OUTRIGHT — the override names the
|
|
35
|
+
* directory the two files sit in directly, with no `audit/` beneath it, which is
|
|
36
|
+
* the contract it has always had and what every test using it expects. Without
|
|
37
|
+
* the override the paths come from `fp-home.ts`, which as of layout 4 puts them
|
|
38
|
+
* under `audit/` with the rest of what the audit owns.
|
|
39
|
+
*/
|
|
30
40
|
export function getAuthDir(): string {
|
|
31
41
|
const override = process.env.FAILPROOFAI_AUTH_DIR;
|
|
32
42
|
if (override) return override;
|
|
33
|
-
return
|
|
43
|
+
return auditDir();
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
export function getAuthFilePath(): string {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/** Location of the persisted re-audit reminder (separate from auth.json so
|
|
41
|
-
* the reminder survives unrelated session refreshes). */
|
|
42
|
-
export function getReminderFilePath(): string {
|
|
43
|
-
return join(getAuthDir(), "next-audit.json");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface StoredReminder {
|
|
47
|
-
/** Unix seconds. */
|
|
48
|
-
next_audit_at: number;
|
|
49
|
-
/** Email the reminder was set for. Used to invalidate the reminder if the
|
|
50
|
-
* active session belongs to a different user. */
|
|
51
|
-
user_email: string;
|
|
52
|
-
/** Unix seconds. */
|
|
53
|
-
set_at: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function readReminder(): StoredReminder | null {
|
|
57
|
-
const p = getReminderFilePath();
|
|
58
|
-
if (!existsSync(p)) return null;
|
|
59
|
-
try {
|
|
60
|
-
const raw = readFileSync(p, "utf-8");
|
|
61
|
-
const parsed = JSON.parse(raw) as Partial<StoredReminder>;
|
|
62
|
-
if (
|
|
63
|
-
typeof parsed.next_audit_at !== "number" ||
|
|
64
|
-
typeof parsed.user_email !== "string" ||
|
|
65
|
-
typeof parsed.set_at !== "number"
|
|
66
|
-
) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
next_audit_at: parsed.next_audit_at,
|
|
71
|
-
user_email: parsed.user_email,
|
|
72
|
-
set_at: parsed.set_at,
|
|
73
|
-
};
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function writeReminder(reminder: StoredReminder): void {
|
|
80
|
-
writeJsonAtomically(getReminderFilePath(), reminder);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function deleteReminder(): void {
|
|
84
|
-
const p = getReminderFilePath();
|
|
85
|
-
if (existsSync(p)) rmSync(p, { force: true });
|
|
47
|
+
const override = process.env.FAILPROOFAI_AUTH_DIR;
|
|
48
|
+
return override ? join(override, "session.json") : auditSessionFile();
|
|
86
49
|
}
|
|
87
50
|
|
|
88
51
|
export function readAuth(): StoredAuth | null {
|
|
@@ -127,6 +90,28 @@ export function writeAuth(auth: StoredAuth): void {
|
|
|
127
90
|
export function deleteAuth(): void {
|
|
128
91
|
const p = getAuthFilePath();
|
|
129
92
|
if (existsSync(p)) rmSync(p, { force: true });
|
|
93
|
+
|
|
94
|
+
// Also any copy a migration took and could not clean up.
|
|
95
|
+
//
|
|
96
|
+
// The layout-4 step backs `auth.json` up before moving it, and a chain that
|
|
97
|
+
// FAILED deliberately keeps that copy — it is the only insurance against a
|
|
98
|
+
// half-moved credential. But "sign me out" has to mean the token is off this
|
|
99
|
+
// machine, and `migrationsDir` is classed `identity`, so nothing else will
|
|
100
|
+
// ever remove it: without this sweep a dashboard sign-out, a 401 auto-delete
|
|
101
|
+
// and `failproofai reset` all left a live bearer and refresh token on disk,
|
|
102
|
+
// to be carried into every dotfile backup and container image after it.
|
|
103
|
+
try {
|
|
104
|
+
const root = migrationsDir();
|
|
105
|
+
if (!existsSync(root)) return;
|
|
106
|
+
for (const entry of readdirSync(root)) {
|
|
107
|
+
if (!entry.startsWith("backup-layout")) continue;
|
|
108
|
+
const copy = resolve(root, entry, "auth.json");
|
|
109
|
+
if (existsSync(copy)) rmSync(copy, { force: true });
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Sign-out must succeed even if the sweep cannot. The live token — the one
|
|
113
|
+
// that actually authenticates a request — is already gone above.
|
|
114
|
+
}
|
|
130
115
|
}
|
|
131
116
|
|
|
132
117
|
/** Convert verify/refresh response into the on-disk shape. */
|
|
@@ -161,8 +146,8 @@ const REFRESH_LEEWAY_SECS = 60;
|
|
|
161
146
|
|
|
162
147
|
/**
|
|
163
148
|
* In-flight refresh dedup. Without this, two concurrent callers (e.g.
|
|
164
|
-
* the dashboard's `/api/auth/status` poll and a
|
|
165
|
-
*
|
|
149
|
+
* the dashboard's `/api/auth/status` poll and a scheduled audit's report
|
|
150
|
+
* in flight) both observe the same expired access token, both call
|
|
166
151
|
* `refreshAccessToken(auth.refresh_token)` with the same refresh token,
|
|
167
152
|
* and the api-server treats the second call as token-replay and revokes
|
|
168
153
|
* every session for that user — a silent logout. Keying on the refresh
|