feat(audit+auth): /audit dashboard, email-OTP auth (CLI + dashboard), SES-backed re-audit reminders, unified pixel-craft design system#397
Conversation
… shareable poster
Adds an in-app audit report at /audit that turns the existing
`failproofai audit` data into a personality-driven dashboard. Every
detector / policy hit feeds a weighted classifier that lands the agent
in one of 8 archetypes; the report uses that to motivate enabling
unenabled builtin policies.
What's new
- Archetype catalog + classifier (`src/audit/archetypes.ts`): 8 archetypes
(optimist, cowboy, explorer, goldfish, paranoid architect, precision
builder, hammer, ghost) with pixel sigils, taglines, "common in" /
"primary risk" copy. `SIGNAL_MAP` maps every builtin policy + every
audit-only detector (47/47 coverage) to an archetype with a tuned
weight. Classifier picks the dominant archetype, falls back to
`goldfish` for broad-spread agents, and to `precision` when no signal
fired.
- Scoring (`src/audit/scoring.ts`): starts at 100, subtracts capped
per-source penalties (deny -1.2, instruct/warn -0.7, sanitize -0.4,
detector -0.5). Grade thresholds S/A/B/C/D/F match the reference.
`projectedScore` previews the post-enable uplift; `syntheticRank`
produces a stable cohort rank from the score.
- Derivations (`src/audit/strengths.ts`, `src/audit/findings.ts`):
Strengths surface real numbers (clean-call %, avg turns, "0 credential
leaks" when sanitize policies didn't fire, etc.). Findings carry
hand-curated body + cost copy per policy slug and the real captured
evidence from `AuditCount.examples`.
- Detector → policy fix mapping (`findings.ts:DETECTOR_TO_POLICY`):
each of the 8 audit-only detectors is paired with the closest real-time
builtin policy, so every finding card shows a real
`$ failproof policy add <slug>` install command — no "audit-only"
framing in the report. Multi-policy mappings render an "also covered
by <policy>" hint. Prescribed-policy section aggregates detector hits
into the target policy with `(via redundant-cd-cwd, …)` attribution.
Sections
01 Identity (archetype hero with sigil + meta grid), 01b Show off CTA,
02 Strengths, 03 Score + cohort leaderboard with distribution histogram,
04 Findings (per-policy cards: what happened / cost / evidence / fix),
05 Prescribed policies (with projected score uplift callout), 06 Return
loop ("re-audit in 7 days"). Server page reads the dashboard cache only;
all derivation is client-side. Catalog size is computed server-side and
passed as a prop (BUILTIN_POLICIES and audit detectors pull in node:fs
via the workflow / require-* policies, so they can't ship to the client).
Cache + API
- Dashboard cache at ~/.failproofai/audit-dashboard.json (mode 0600,
single slot, new runs overwrite). Helper at
`src/audit/dashboard-cache.ts` (read/write/staleness). Schema bumped
to `version: 2` with new fields `eventsScanned: number` (total
tool-use events scanned, drives the "X tool calls" headline),
`projectsScanned: string[]` (drives the project filter), and
`enabledBuiltinNames: string[]` (lets findings answer "is this fix
already enabled?" without iterating result rows).
- POST /api/audit/run calls runAudit() in-process, writes the dashboard
cache, and serializes via a module-scoped singleton lock so concurrent
clicks 409. GET /api/audit/status reports {running, startedAt,
cachedAt} for client polling. Server action
`app/actions/get-audit-result.ts` reads the cache without triggering
a run, mirroring the `/policies` `getHooksConfigAction()` pattern.
Re-run UX
- Empty state CTA on first visit; in-flight re-runs render a four-stage
faux progress UI (`run-progress.tsx`). RerunButton POSTs `/api/audit/run`
with the current scan params, polls `/api/audit/status` at 1Hz, and
refetches via the server action when running flips false.
- Shareable PNG export: clicking "make poster" captures the identity
archetype-frame DOM via html2canvas at scale 2 and downloads
`failproofai-<archetype>-<YYYY-MM-DD>.png`. New dependency:
html2canvas@^1.4.1.
Styling
- Ported `assets/audit/styles.css` (1235 lines) verbatim into
`app/audit/audit-styles.css`, scoped to the route via page-level
import. JetBrains Mono + VT323 loaded from Google Fonts; Architype
Stedelijk shipped locally under public/audit/fonts/. Reference design
kit (audit.jsx / poster.jsx / tweaks-panel.jsx / styles.css /
archetypes.jsx + screenshots) committed under assets/audit/ for
future iteration. ESLint config gains an assets/ ignore so the design
kit's vanilla React-Babel JSX isn't linted as project source.
Animation primitives in app/globals.css: `.audit-row-enter` (staggered
fade-up via `--row-delay`) and `.audit-bar-fill` (width 0 → `--bar-width`
on mount), both honoring `prefers-reduced-motion`.
Navbar / layout
- Navbar gains an "Audit" entry between Policies and Projects with a
ClipboardCheck icon and an optional slipping-count chip (rendered
when the layout's server-side cache read finds >0 slipping hits).
Layout passes the count via a new `auditSlippingCount` prop.
Core changes (additive, original policies untouched)
- `src/hooks/policy-registry.ts`: added `getAllPolicies()` and
`setAllPolicies()` exports for snapshot/restore. Existing
`registerPolicy` / `clearPolicies` / `getPoliciesForEvent` /
`normalizePolicyName` semantics unchanged.
- `src/audit/replay.ts`: `initReplay()` now snapshots the registry via
`getAllPolicies()` before clearing it; new `restoreReplay()` puts the
pre-init policies back. `runAudit()` wraps the work in try/finally so
embedding the audit in long-running processes (the Next.js dashboard
is one) no longer wipes pre-existing registrations.
- `src/audit/index.ts`: surfaces `eventsScanned`, `projectsScanned`,
`enabledBuiltinNames` on the result; per-transcript scan now tracks
events count + cwd. Schema-version bump 1 → 2.
Tests
- New `__tests__/audit/dashboard-cache.test.ts` (round-trip, 0600 mode,
corrupt-JSON resilience, staleness threshold).
- `__tests__/audit/replay.test.ts` adds three tests covering registry
snapshot/restore: a user-registered policy survives `initReplay()` →
`restoreReplay()`, `restoreReplay()` is idempotent, and calling
`restoreReplay()` before `initReplay()` is a no-op.
- Full suite green: 1701 / 1701.
Verification
- `bunx tsc --noEmit` clean
- `bun run lint` 0 errors (2 pre-existing warnings retained)
- `bun run test:run` 1701 / 1701
- `bun --bun next build` succeeds; new routes `/audit`,
`/api/audit/run`, `/api/audit/status` all registered
- Hook handler smoke against live config (`block-failproofai-commands`
fires deny on `failproofai policies --uninstall`, harmless commands
pass cleanly) — runtime policy enforcement intact
Adds the missing CHANGELOG entry for the /audit dashboard work and a new "### Audit" section under docs/dashboard.mdx's Pages list. Also appends `audit` to the FAILPROOFAI_DISABLE_PAGES valid-values list (the page-level disable gate added in app/audit/page.tsx already honors it; the docs were one step behind). Translated dashboard.mdx mirrors (14 locales) are intentionally left for the translation-sync workflow — same pattern as the env-vars docs from 0.0.11-beta.2.
- Expand every archetype in src/audit/archetypes.ts to a multi-variant catalog (4–6 taglines, keyword sets, descriptions, signature blocks, common-in / primary-risk / closing lines per archetype). A new pickArchetypeVariant(key, seed) resolver picks one variant per field via a djb2-hashed, per-field-axis index, so the persona blurb stays stable for a given project seed but two projects landing on the same archetype see different copy. IdentitySection consumes the resolved variant; the seed flows from audit-dashboard.tsx as the inferred project name. Fix the picker's signed-modulo bug (final XOR re-introduced signedness → negative index → undefined keywords) by forcing >>> 0 on the final mix. - Simplify return-section's CTA to '[ install policies ]' copying the bare `failproofai policies --install` command (no per-policy short names appended). - Fix the [ share → ] header button: replace scrollIntoView with a manual window.scrollTo that subtracts the sticky .app-header height (+16px breathing room), plus a scroll-margin-top: 80px fallback on .showoff. - Harden the 'make poster' PNG export so the captured archetype frame no longer collides with the sigil / tagline: await document.fonts.ready before capture, apply a .capturing class that locks every clamp()'d font-size and grid column to an absolute value tuned for the 1100px capture width, drop text-shadow / box-shadow that html2canvas crops unpredictably, and capture with a 12px bleed on every side so the frame's corner accents survive the crop.
…pi-server
Implements end-to-end email-OTP auth against the Rust failproof-api-server,
exposed through both the `failproofai auth` CLI subcommand and the in-app
dashboard. Adds a gating step on the /audit page's "set a reminder" CTA so
unidentified visitors can verify themselves inline before reminders get
queued (mail-scheduling itself is deferred).
Architecture
CLI (failproofai auth) Dashboard (Next.js)
\\ /
\\ reads/writes / reads/writes
\\ /
~/.failproofai/auth.json (mode 0600)
|
| bearer JWT
v
failproof-api-server (Rust) -> Postgres
CLI surface (src/auth/cli.ts, dispatched from bin/failproofai.mjs)
failproofai auth --login Email + OTP flow, writes auth.json
failproofai auth --logout Revokes server-side, wipes auth.json
failproofai auth --whoami Prints identity from /me (silent refresh)
failproofai auth --help Usage
Readline input-masking is TTY-gated so piped stdin (tests / scripts) doesn't
stall on the per-character _writeToOutput callback.
Shared HTTP + persistence layer (lib/auth/)
api-server-client.ts Stateless fetch client. Endpoint helpers
(requestLoginCode, verifyLoginCode,
refreshAccessToken, logoutSession, fetchMe,
decodeJwt). AuthApiError carries status, code,
retry_after_secs. Base URL from FAILPROOF_API_URL
(default http://localhost:8080). Tolerates both
the documented {code,message} error shape and the
live server's {success,code,detail} shape.
auth-store.ts File persistence at ~/.failproofai/auth.json,
mode 0600 (creation + chmodSync on overwrite).
getValidAccessToken() auto-refreshes within a 60s
leeway; whoAmI() does one refresh-and-retry on a
hard 401 then wipes the file. FAILPROOFAI_AUTH_DIR
env-var override exists for tests.
Dashboard API routes (app/api/auth/)
GET /api/auth/status {authenticated, user?} via whoAmI()
POST /api/auth/login-request Proxy; surfaces retry_after_secs
POST /api/auth/login-verify Proxy; on 200 persists tokens locally and
returns ONLY {authenticated, user} -- the
refresh token never reaches the browser
POST /api/auth/logout Revokes upstream, deletes auth.json
regardless of upstream success
Dashboard UI
app/audit/_components/auth-dialog.tsx
Modal dialog matched to /audit's pixel-craft aesthetic: pink corner
brackets, dashed-frame backdrop, terminal mono inputs, masked OTP
entry, live 30s resend countdown, ESC / backdrop / [x] close, error
banner with rate-limit messaging.
app/audit/_components/return-section.tsx
Probes /api/auth/status on mount. [set a reminder] gates on auth:
unknown -> button disabled, anon -> opens dialog with "oops -- you
are unknown", authed -> flashes [reminder queued for <email>] and
shows a green "signed in as <email>" pill under the CTA.
app/audit/audit-styles.css
New .auth-dialog* + .auth-status-pill rules using the existing color
palette and font stack.
Production deploy hooks
- Set FAILPROOF_API_URL on the user's machine OR change DEFAULT_API_BASE
in lib/auth/api-server-client.ts to the prod URL before publishing.
The npm package never touches Postgres directly -- only the HTTP
surface of the api-server. Database / JWT / SES config all live with
the api-server deployment.
- CORS work is not needed: every browser-visible auth call goes through
the Next.js API routes (server-side), so the api-server never sees a
cross-origin browser request.
- Refresh-token reuse detection happens on the api-server (rotated_to
chain); the client treats any 401-from-refresh as "wipe local
session" so theft-revoked users get pushed back to the login dialog.
Local dev loop
1. docker run -d --rm --name failproof-pg -e POSTGRES_PASSWORD=postgres \\
-e POSTGRES_USER=postgres -e POSTGRES_DB=failproof -p 5544:5432 \\
postgres:16-alpine
2. cd platform/failproofai/api-server && \\
FAILPROOF_DATABASE_URL=postgres://postgres:postgres@localhost:5544/failproof \\
FAILPROOF_JWT_SIGNING_KEY=<>=32-byte-string> \\
FAILPROOF_BIND_ADDR=127.0.0.1:8080 \\
FAILPROOF_EMAIL_SENDER_BACKEND=log FAILPROOF_ENVIRONMENT=local \\
cargo run --bin server
3. FAILPROOF_API_URL=http://127.0.0.1:8080 bun run dev
4. In a fourth terminal:
bun bin/failproofai.mjs auth --login
OTP appears in the api-server's stdout under the
"login code (dev log sender)" log line.
Verified end-to-end against a Docker postgres + the api-server: CLI login
+ whoami + logout, all four dashboard routes, the audit page rendering
with the gated reminder button, and the shared auth.json across both
surfaces (sign in via CLI -> dashboard sees it; logout via CLI ->
dashboard reverts to anonymous on next page load). Existing 1701 vitest
tests, eslint, and tsc all stay green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…I_URL Documents the new `failproofai auth --login | --logout | --whoami` subcommand and the two env-var knobs (`FAILPROOF_API_URL`, `FAILPROOFAI_AUTH_DIR`) shipped with the auth feature. i18n mirrors will pick this up via the existing translate-docs workflow on the next sync. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…yle leak
Promotes the /audit page's design language to the whole app so /policies and
/projects pick up the same fonts, palette, chrome, and component vocabulary.
Also fixes a latent bug where the audit page's :root + body resets persisted
on client-side navigation back to other routes, leaving them with audit's
JetBrains Mono / dark canvas but none of the matching section chrome.
Strategy: unify at the CSS-variable level. Every shadcn-style Tailwind token
(--background, --card, --foreground, --primary, --border, --radius, …) is
repointed at the audit palette (--bg, --bg-2, --ink, --accent-pink, …) in
globals.css. Existing Tailwind utility classes like `bg-card text-foreground
border-border` continue to work but now produce audit visuals — no component
rewrites needed for the 1661-line hooks-client tree.
Files changed
app/globals.css Rewritten. Single source of truth for fonts, tokens,
body atmosphere (cross-hatch + grain + pink vignette),
and every shared chrome class (.app-header / .h-brand
/ .btn / .btn-press / .tabs / .tab / .section /
.section-mast / .section-h / .report / new .panel).
app/audit/audit-styles.css
Trimmed by 150 lines. Drops :root, the html/body/#root
resets, the body atmosphere overlays, .app-header,
.btn, .tabs — all now live in globals. Keeps only the
/audit-only widgets (archetype-frame, sigil, score
grade, leaderboard, findings, return hook, auth
dialog). Side effect: nothing left to leak.
app/layout.tsx Removes the next/font/google Geist Mono import. Fonts
ship via the @import url(…JetBrains+Mono…) in
globals.css so the design system is one stylesheet.
components/navbar.tsx Rewritten around .app-header. Pink "▮▮" pixel mark +
Architype Stedelijk wordmark, optional version chip,
dynamic per-section eyebrow ("policies" / "audit" /
"projects"), .tab links with sharp pink underline on
the active route. Drops lucide icons from the bar.
app/projects/page.tsx + loading.tsx
Wrapped in .report + .section + .panel. New
green-eyebrow masthead with the ━━ glyph and
"your agent footprint." section heading. Empty and
loaded states both use the dashed-frame .panel.
ProjectList component itself unchanged.
app/policies/hooks-client.tsx
Top-level <div className="min-h-screen bg-background
…"> replaced with a .report + .section shell. New
masthead with audit-style copy ("what your agents
tried." / "what to stop them doing.") and an enabled-
count meta chip in pink. TabBar swapped from rounded
pill to global .tabs / .tab with sharp pink underline
on active. Dropped the unused ArrowLeft + back-to-
projects link (navbar handles cross-page nav now).
No inner refactor of ActivityTab / PoliciesTab.
Verification
bunx tsc --noEmit passes
bun run lint passes (only the 2 pre-existing warnings)
bun run test:run 1701/1701 pass
bun --bun next build Compiled successfully in 6.2s; static + dynamic
routes for /, /policies, /projects, /audit, and all
/api/auth and /api/audit endpoints generated.
The user needs to restart `bun run dev` once after pulling this commit — the
Turbopack HMR pipeline can't hot-swap :root / @import changes reliably.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t now
Six polish items in one pass — sizing, second-navbar fix, score-section
rewrite, empty/running restyle, and persistent reminder state across
sessions.
Sizing
globals.css: base 13 → 14.5px, .report max-width 1180 → 1380px (40px
side padding), .section padding restored to 64px. Default-zoom
readability across /audit, /policies, /projects no longer forces a
browser zoom-in.
Double navbar
Delete app/audit/_components/app-header.tsx and all three of its
mount sites in audit-dashboard.tsx (cached, in-flight, and
ShellEmpty). The global navbar already supplies brand + tabs + reach;
the in-page bar with [share →] was redundant chrome.
Score section
Drop the synthetic cohort leaderboard. Replace ScoreSection with a
single .panel (.score-share-card) split into score + share:
left — big tier-colored score, tier badge, progress bar to the
next grade band, 3 stat boxes (missing policies, pts to
next tier, est. days to fix), policy-status chip strip
right — X + LinkedIn pre-written templates derived from
score/archetype/missing; [share on X], [share on
LinkedIn], [download audit card] (html2canvas captures
the entire panel as failproofai-card-<grade>-<score>.png)
audit-dashboard.tsx drops the unused syntheticRank import / rank
prop and threads `result` into the new section.
Empty / running
empty-state.tsx: shadcn Button + lucide icon center card → .panel
with a 6×6 pixel-grid sigil, Architype Stedelijk headline,
.btn-press CTA, audit-style meta caption. Mode "no-cache" → "run
your first audit." with [ run audit ]. Mode "zero-sessions" →
"install hooks first." with [ install guide → ].
run-progress.tsx: terminal-style panel — "$ failproofai audit
--since 30d ▮" header with a blinking pink cursor, stage list with
✓ / ▮▮ / ○ markers + per-stage braille spinner, marquee progress
bar with a pink shine sweep.
Persistent reminder
~/.failproofai/next-audit.json — separate from auth.json so a token
refresh / re-login doesn't churn the reminder. Mode 0600, same
perms hygiene as auth.json (writeFileSync with mode + post-write
chmodSync on overwrite).
lib/auth/auth-store.ts: new readReminder / writeReminder /
deleteReminder / getReminderFilePath + StoredReminder type.
app/api/auth/reminder/route.ts: GET / POST / DELETE. POST defaults
to a 7-day offset; reminder is scoped to the active session so a
reminder for a@x.com is invisible when b@x.com is the live CLI
session.
/api/auth/status returns `reminder: { next_audit_at, user_email,
set_at } | null` alongside the user.
Return section
Behavior matrix in return-section.tsx:
unknown → buttons disabled while /api/auth/status is in flight
anon → [set a reminder] opens AuthDialog, on success persists
the 7-day reminder automatically (no second click)
authed + no reminder → [set a reminder] writes the timestamp
directly, no dialog
authed + reminder set → status panel showing
"next audit set for <Mon Jun 8> · in 7 days" and
"signed in as <email>", plus [re-audit now] /
[install policies] / "clear reminder"
[re-audit now] button is exposed to all authed states (plus anon,
next to install-policies). It reuses triggerRun() from
rerun-button.tsx and reloads the page once the new run finishes.
Verification
bunx tsc --noEmit passes
bun run lint passes (only the 2 pre-existing warnings)
bun run test:run 1701/1701 pass
bun --bun next build Compiled successfully — new
/api/auth/reminder route registers alongside /api/auth/{status,
login-request, login-verify, logout}.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The new persistent re-audit reminder ships a small companion file alongside auth.json. Add a short section to docs/cli/auth.mdx covering its shape, the per-email scoping rule (so swapping CLI accounts hides the previous user's reminder), the 0600 perms, and the GET / POST / DELETE /api/auth/reminder endpoint that backs the UI button. CHANGELOG Docs entry matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds email OTP auth (CLI + local token store + Next.js proxy routes + reminder API) and a new /audit dashboard with caching, run/status endpoints and module lock, archetype classification, scoring/grades/cohort, findings/policies UIs, poster PNG export, global design-system CSS, standalone audit assets, tests, and docs. ChangesEmail OTP Authentication & Audit Dashboard
Estimated code review effort: Possibly Related PRs
|
There was a problem hiding this comment.
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/policies/hooks-client.tsx (1)
1573-1584:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winKeep the parent summary state in sync after configure-tab changes.
This effect seeds
policyCountsandinstalledCliLabelsonce, butPoliciesTabcan later install/remove CLIs and toggle policies. After those updates, the section meta and activity summary below keep showing the pre-change values until a full reload. Refresh these derived values from the same reload path you already use inPoliciesTab, or lift that summary state into a shared refresh function.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/policies/hooks-client.tsx` around lines 1573 - 1584, The current useEffect seeds policyCounts and installedCliLabels only once via getHooksConfigAction(), so later changes in PoliciesTab are not reflected; extract the fetch-and-set logic into a shared refresh function (e.g., fetchHooksConfig) that calls getHooksConfigAction() and then calls setHooksInstalled, setPolicyCounts, and setInstalledCliLabels, update the useEffect to call that function, and have PoliciesTab call the same fetchHooksConfig after any CLI install/remove or policy toggle so the parent summary state stays in sync.
🟠 Major comments (27)
app/api/auth/reminder/route.ts-94-96 (1)
94-96:⚠️ Potential issue | 🟠 Major | ⚡ Quick winScope reminder deletion to the active session.
DELETEunconditionally wipesnext-audit.json. That means an anonymous caller, or a different signed-in user on the same host, can clear someone else's persisted reminder even though GET/POST are user-scoped.Require
whoAmI()here and only delete when the stored reminder belongs to that session; otherwise return401/404.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/api/auth/reminder/route.ts` around lines 94 - 96, The DELETE handler currently calls deleteReminder() unconditionally; change it to require whoAmI() and scope deletion to the active session: call whoAmI() at the start of DELETE, return 401 if it yields no authenticated session, fetch the stored reminder (the same store used by GET/POST), verify the reminder’s owner/session id matches the whoAmI() result, and only then call deleteReminder(); if no reminder exists or it belongs to someone else return 404 (or 401 for unauthenticated), otherwise return NextResponse.json({ ok: true }) after deletion. Use the existing function names (DELETE, whoAmI, deleteReminder) to locate and update the logic.src/auth/cli.ts-126-133 (1)
126-133:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't block
--loginon a staleauth.json.
readAuth()only proves the file exists. If the stored session is expired or already revoked, this early return forces the user to discover and runfailproofai auth --logoutbefore they can log back in.Use
whoAmI()orgetValidAccessToken()before short-circuiting, and fall through to the login flow when the stored session can't be recovered.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/auth/cli.ts` around lines 126 - 133, The early return in runLogin() uses readAuth() to detect an existing auth file but doesn't verify that the session is still valid; update runLogin() to validate the stored session (call whoAmI() or getValidAccessToken() using the data returned by readAuth()) and only short-circuit when the token is confirmed valid; if validation fails or throws, fall through to the interactive login flow and remove the current early-return behavior so stale/revoked tokens do not block --login.lib/auth/auth-store.ts-64-247 (1)
64-247: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy liftAdd regression tests for the new persistence and refresh flows.
This file adds disk validation, refresh-on-expiry, refresh-on-401, and cross-user reminder scoping, but there’s no accompanying
__tests__coverage in this review set. These paths are stateful and easy to regress.Based on learnings/coding guidelines: "Always add unit tests for new behaviour. Place tests in tests/."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/auth/auth-store.ts` around lines 64 - 247, You need to add unit tests under __tests__ covering the new persistence and refresh behaviours: write tests for readReminder/writeReminder/deleteReminder and readAuth/writeAuth/deleteAuth to validate on-disk validation and permission handling (use mocks/fs temp dirs), tests for authFromTokenResponse and readAccessExpiry (decodeJwt) to assert token→StoredAuth conversion and exp parsing, tests for getValidAccessToken to exercise successful no-refresh, refresh-on-expiry, and refresh failure paths (mock refreshAccessToken and AuthApiError), and tests for whoAmI to cover normal fetchMe, refresh-on-401 retry (mock readAuth/refreshAccessToken/fetchMe) and unrecoverable 401 leading to deleteAuth; place these tests in __tests__ and use time mocking (Date.now), fs mocks or temp filesystem, and mocking of refreshAccessToken/fetchMe to simulate network and error cases.app/api/auth/reminder/route.ts-62-67 (1)
62-67:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReject malformed JSON instead of silently scheduling the default reminder.
req.json().catch(() => ({}))makes an invalid body behave like an empty one, so a bad client request still writes a 7-day reminder. This should be a400, not a successful mutation.Treat only an actually empty body as "use defaults"; malformed JSON should fail validation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/api/auth/reminder/route.ts` around lines 62 - 67, Remove the silent .catch that converts malformed JSON to {} and instead detect and reject invalid JSON: read the raw request body via req.text(), if the text is empty or only whitespace then set body = {} (use SetBody) to apply defaults; otherwise attempt JSON.parse on the text (or call JSON.parse after text) and if parsing throws return a 400 response. Update the code around the existing body/SetBody logic in the route handler that currently does body = (await req.json().catch(() => ({}))) as SetBody so malformed JSON results in a 400 while truly empty bodies still use the default 7-day offset.lib/auth/auth-store.ts-87-97 (1)
87-97:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftMake the shared JSON writes atomic.
These files are intentionally shared between the CLI and the dashboard, but both write paths replace the target file in place. A concurrent write or process crash can leave truncated JSON behind, which turns into a silent logout/reminder loss on the next read.
Suggested direction
+import { renameSync } from "node:fs"; + +function writeJsonAtomically(path: string, value: unknown): void { + const tmp = `${path}.tmp`; + writeFileSync(tmp, JSON.stringify(value, null, 2), { mode: 0o600 }); + try { + if (statSync(tmp).mode & 0o077) chmodSync(tmp, 0o600); + } catch { + // best-effort + } + renameSync(tmp, path); +} + export function writeReminder(reminder: StoredReminder): void { const p = getReminderFilePath(); const dir = dirname(p); if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 }); - writeFileSync(p, JSON.stringify(reminder, null, 2), { mode: 0o600 }); - try { - if (statSync(p).mode & 0o077) chmodSync(p, 0o600); - } catch { - // best-effort - } + writeJsonAtomically(p, reminder); } export function writeAuth(auth: StoredAuth): void { const p = getAuthFilePath(); const dir = dirname(p); if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 }); - writeFileSync(p, JSON.stringify(auth, null, 2), { mode: 0o600 }); - try { - if (statSync(p).mode & 0o077) chmodSync(p, 0o600); - } catch { - // best-effort - } + writeJsonAtomically(p, auth); }Also applies to: 139-152
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/auth/auth-store.ts` around lines 87 - 97, The writeReminder function performs in-place writes which can produce truncated JSON on concurrent writes or crashes; change it (and any other file-writers in this module) to perform atomic replace by writing JSON to a temp file in the same directory (use a randomized suffix), set the temp file permissions to 0o600, fsync the temp file, rename the temp file to the real path (atomic on POSIX), then fsync the directory; keep the existing best-effort chmod/exists logic but ensure errors are handled and cleaned up (unlink temp on error) so readers never see a partially written file.lib/auth/auth-store.ts-201-209 (1)
201-209:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftDon't collapse transient upstream failures into "logged out".
Both refresh and
/meverification returnnullfor network/upstream errors, so/api/auth/statusends up reportingauthenticated: falseduring transient outages. That turns a temporary auth-service problem into an anonymous state change in the UI.A separate "unavailable" path here would let callers preserve the last known auth state instead of dropping the session on transport errors.
Also applies to: 223-240
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/auth/auth-store.ts` around lines 201 - 209, The current catch blocks in the auth-store (which check for AuthApiError and call deleteAuth()) collapse network/upstream errors into a "logged out" null return; instead, leave null only for unrecoverable 401 cases and return a distinct "unavailable" signal for transient failures so callers can preserve the last known session. Concretely: in the catch blocks that reference AuthApiError and call deleteAuth(), keep the deleteAuth()+null behavior for err instanceof AuthApiError && err.status === 401, but for all other errors return or throw a dedicated marker (e.g., AuthUnavailable / throw new AuthUnavailableError) rather than null; apply this change to both catch sites (the refresh/token path and the /me verification path) and update consumers (e.g., getAuthStatus or the /api/auth/status handler) to treat that marker as "service unavailable" instead of clearing the session.lib/auth/api-server-client.ts-99-118 (1)
99-118:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd a timeout to the shared upstream fetch helpers.
Both helpers can wait on
fetch()indefinitely. In the CLI that can hangfailproofai authforever, and in the dashboard routes it can pin a request until the platform times it out.Suggested fix
+const FETCH_TIMEOUT_MS = 10_000; + async function postJson<T>(path: string, body: unknown, init?: { accessToken?: string }): Promise<T> { const headers: Record<string, string> = { "content-type": "application/json" }; if (init?.accessToken) headers["authorization"] = `Bearer ${init.accessToken}`; const res = await fetch(`${getApiBase()}${path}`, { method: "POST", headers, body: JSON.stringify(body), + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), }); if (res.status === 204) return undefined as T; if (!res.ok) throw await parseError(res); return (await res.json()) as T; } async function getJson<T>(path: string, accessToken: string): Promise<T> { const res = await fetch(`${getApiBase()}${path}`, { method: "GET", headers: { authorization: `Bearer ${accessToken}` }, + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), }); if (!res.ok) throw await parseError(res); return (await res.json()) as T; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/auth/api-server-client.ts` around lines 99 - 118, postJson and getJson can hang indefinitely because fetch has no timeout; fix both by using an AbortController: create an AbortController inside postJson and getJson, pass controller.signal to fetch, start a timeout (configurable or default, e.g., 10s) that calls controller.abort(), and clear the timeout after fetch completes; ensure any aborts propagate as errors (so existing parseError handling still runs) and that you pass the signal in the fetch init for functions postJson and getJson.src/audit/archetypes.ts-894-939 (1)
894-939: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winAdd direct unit coverage for the classifier branches.
This introduces three distinct outcome rules plus the 40% secondary cutoff, but the supplied
__tests__changes don't exercise any of them. Please add table-driven cases for zero-signal →precision, broad-spread →goldfish, and secondary fallback vs. promotion so these thresholds stay stable.As per coding guidelines "Always add unit tests for new behaviour. Place tests in tests/."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/audit/archetypes.ts` around lines 894 - 939, Add direct unit tests for classifyAgent to cover its three branching rules: create table-driven tests in __tests__ that (1) pass an AuditResult with no signals (result.results empty or hits=0) and assert archetype === "precision" and totalSignal === 0; (2) craft results that map via SIGNAL_MAP to at least 5 non-zero archetypes with top3Sum/totalSignal < 0.6 and assert archetype === "goldfish" and secondary equals the highest-weighted archetype; and (3) exercise the 40% secondary cutoff by building two cases where the second-highest weight is just above 40% of primary (expect secondary promoted to sorted[1][0]) and just below 40% (expect secondary === ARCHETYPES[primary].secondary); reference classifyAgent, SIGNAL_MAP, ARCHETYPES and AuditResult when constructing those cases.app/audit/_components/identity-section.tsx-43-47 (1)
43-47:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't claim all key policies are live for every A-tier result.
The positive branch keys off
grade === "A", but A-tier audits can still havemissing > 0. That makes the shared LinkedIn copy contradict the actual findings for some users.📝 Suggested fix
function buildLinkedInTemplate(score: number, archetypeName: string, grade: Grade, missing: number): string { - const verdict = (grade === "S" || grade === "A") + const verdict = missing === 0 ? `${score}/100 — ${grade} tier. every key policy is live. the audit confirmed what good looks like.` : `${score}/100 — ${grade} tier. ${missing} prescribed polic${missing === 1 ? "y" : "ies"} uncovered — each one is a real attack surface.`; return `We ran a failproofai security audit on our AI agent stack.\n\n${verdict}\n\nArchetype: ${archetypeName.toLowerCase()}. failproofai maps your agent\'s behavior pattern, identifies the exposure, and prescribes the exact policies to close it.\n\nFree. Open-source. 30 seconds to run: ${SITE_URL}`; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/audit/_components/identity-section.tsx` around lines 43 - 47, The LinkedIn copy in buildLinkedInTemplate incorrectly assumes all A-tier results have no missing policies; change the conditional that builds `verdict` so that the positive branch requires both `grade === "S" || grade === "A"` AND `missing === 0`, e.g., check `grade === "S" || (grade === "A" && missing === 0)`, so that when `missing > 0` the template uses the negative branch which mentions uncovered policies; update only the condition used to select the message (leave the wording of each branch intact) to ensure the copy matches actual findings.src/audit/scoring.ts-97-112 (1)
97-112:⚠️ Potential issue | 🟠 Major | ⚡ Quick winCap projected recovery with the same buckets as
deriveScore.
projectedScoreadds back raw hit weights, butderiveScoreonly ever subtracts up to 25/15/10 per bucket. Once a bucket is already capped, extra hits keep inflating the projection even though they never lowered the current score, so this can promise a much larger jump than enabling the policies can actually produce.🔧 Suggested fix
export function projectedScore(result: AuditResult, currentScore: number): number { - let recoverable = 0; + let denyRecoverable = 0; + let instructRecoverable = 0; + let sanitizeRecoverable = 0; for (const row of result.results) { if (row.source !== "builtin") continue; if (row.enabledInConfig) continue; - if (row.severity === "deny") recoverable += row.hits * 1.2; - else if (row.severity === "instruct" || row.severity === "warn") recoverable += row.hits * 0.7; - else recoverable += row.hits * 0.4; + if (row.severity === "deny") denyRecoverable += row.hits * 1.2; + else if (row.severity === "instruct" || row.severity === "warn") instructRecoverable += row.hits * 0.7; + else sanitizeRecoverable += row.hits * 0.4; } - const proj = Math.min(92, currentScore + Math.round(recoverable)); + const recoverable = + Math.min(denyRecoverable, 25) + + Math.min(instructRecoverable, 15) + + Math.min(sanitizeRecoverable, 10); + const proj = Math.min(92, currentScore + Math.round(recoverable)); return Math.max(currentScore, proj); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/audit/scoring.ts` around lines 97 - 112, projectedScore currently sums weighted recoverable hits directly, which can exceed the per-severity caps used by deriveScore; modify projectedScore to aggregate recoverable points by severity (for builtin && !enabledInConfig rows) and apply the same per-bucket caps as deriveScore (e.g., deny cap 25, instruct cap 15, warn/other cap 10) before summing them, then proceed with Math.min(92, currentScore + Math.round(cappedRecoverable)) and Math.max(currentScore, proj); update the loop in projectedScore to compute per-severity totals, apply the caps, and then combine.src/audit/findings.ts-210-298 (1)
210-298: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winAdd unit tests for detector mapping and card derivation.
This file adds ranking, detector-to-policy remapping, enabled-state logic, relative-time formatting, and fallback copy without any targeted unit coverage. A few focused tests in
__tests__/audit/would catch regressions here quickly.As per coding guidelines, "Always add unit tests for new behaviour. Place tests in tests/."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/audit/findings.ts` around lines 210 - 298, Add unit tests under __tests__/audit/ that exercise deriveFindings and buildCard behavior: create AuditResult fixtures with builtin rows and detector rows to verify sorting by hits, detector-to-policy remapping via DETECTOR_TO_POLICY, that fix slug uses mapping.primary when present, that POLICY_META fallbacks (displayTitle/impact) and FINDING_COPY fallbacks are used for body/cost/desc, that alreadyEnabled logic respects enabledSet and enabledInConfig, that evidence caps at 4 and adds a "no example commands captured." comment when empty, and that lastSeen is formatted via relTimeAgo; assert expected FindingCard fields (num, title lowercased, install command `failproof policy add ${slug}`, projects, count, and alsoCoveredBy) for each case.src/audit/strengths.ts-39-50 (1)
39-50:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftClean-rate is computed from finding hits, not dirty tool calls.
Line 42 subtracts
totals.hitsfromeventsScanned, buttotals.hitsincrements once per policy/detector fire. A single tool call can contribute multiple hits, so this can understate cleanliness or clamp to0%on mixed audits. Either aggregate a distincteventsWithHitscount upstream or stop labeling this as "clean tool calls".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/audit/strengths.ts` around lines 39 - 50, The cleanRate calculation is wrong because totals.hits counts detector hits (can be many per event) but the UI labels it as "clean tool calls"; update the code in the block around variables events, totalHits, detectorsTriggered, cleanRate and the out.push call so it either uses a distinct eventsWithHits count (if available upstream) to compute cleanRate as (events - eventsWithHits)/events, or if that upstream count is not available, stop implying per-tool-call cleanliness: compute a hit-based rate (totalHits / events or hits-per-event) and change the unit/headline/detail text in the out.push to reflect "hits" (e.g., "clean hits" or "hit-based clean rate") instead of "clean tool calls". Ensure updates target the cleanRate variable and the corresponding unit/headline/detail strings.src/audit/strengths.ts-37-138 (1)
37-138: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winAdd unit coverage for the new strengths derivation.
This module adds user-facing ranking and fallback logic, but there isn't a matching test suite for cases like the clean-rate headline, zero-hit gates, and the 5-item cap. Please add coverage under
__tests__/audit/.As per coding guidelines, "Always add unit tests for new behaviour. Place tests in tests/."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/audit/strengths.ts` around lines 37 - 138, Add unit tests for deriveStrengths to cover its new ranking and fallback logic: write tests under __tests__/audit/ that call deriveStrengths with crafted AuditResult fixtures exercising (1) the clean-rate headline when eventsScanned > 0 and detectorsTriggered > 0, (2) the zero-credential gate by ensuring hitsForShort returns 0 for credentialPolicies, (3) the retry/gitrewrite/wasteful-edit zero-hit gates (use inputs that drive retryHits, gitHits, wastefulEdits to 0), (4) average session length branches (avgTurns <15, between 15–29, and >=30), and (5) the cap-to-5 behavior and the fallback “audit complete” entry when out.length < 2. Use the deriveStrengths function from strengths.ts and create minimal AuditResult fixtures (mock totals.hits, transcripts.scanned, eventsScanned, results array) and assert Strength array contents (metrics, headlines, units) for each scenario; mock or stub hitsForShort behavior if needed to target specific policy groups.src/audit/findings.ts-290-293 (1)
290-293:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse the real CLI install command in the fix CTA.
The finding cards currently emit
failproof policy add ..., but the rest of the audit flow usesfailproofai policies --install .... Copying this from the dashboard will send users to a different command surface than the one exposed elsewhere in this PR.💡 Proposed fix
- install: `failproof policy add ${fixSlug}`, + install: `failproofai policies --install ${fixSlug}`,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/audit/findings.ts` around lines 290 - 293, The fix object's install CTA currently uses the wrong CLI; update the install string in the fix block (where fix: { slug: fixSlug, desc: fixDesc, install: ... } is defined) to use the consistent CLI command used elsewhere: `failproofai policies --install ${fixSlug}` so the generated card copies the same command surface as the rest of the audit flow.src/audit/dashboard-cache.ts-40-48 (1)
40-48:⚠️ Potential issue | 🟠 Major | ⚡ Quick winInvalidate old or null cache shapes before returning them.
The current guard accepts
params: null,result: null, and any historicalAuditResultversion. After this schema bump, an existing on-disk v1 cache will still be returned even though it lacksprojectsScanned,eventsScanned, andenabledBuiltinNames, which can break the dashboard until the user re-runs the audit.💡 Proposed fix
const raw = readFileSync(cachePath, "utf-8"); const entry = JSON.parse(raw) as DashboardCacheEntry; if ( typeof entry?.cachedAt !== "string" - || typeof entry?.params !== "object" - || typeof entry?.result !== "object" + || !entry?.params + || typeof entry.params !== "object" + || !entry?.result + || typeof entry.result !== "object" + || entry.result.version !== 2 + || !Array.isArray(entry.result.projectsScanned) + || typeof entry.result.eventsScanned !== "number" + || !Array.isArray(entry.result.enabledBuiltinNames) ) { return null; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/audit/dashboard-cache.ts` around lines 40 - 48, The guard in dashboard-cache.ts currently accepts null or old-version shapes; update the validation after JSON.parse (the const entry = JSON.parse(raw) as DashboardCacheEntry line) to return null if entry.params or entry.result are null or not plain objects, and also verify required v2 AuditResult fields exist and have correct types (e.g., entry.result.projectsScanned is a number, entry.result.eventsScanned is a number, and entry.result.enabledBuiltinNames is an array). Keep the cachedAt string check, and ensure any missing/incorrectly-typed required fields cause the function to return null so only up-to-date DashboardCacheEntry shapes are returned.app/api/audit/run/route.ts-50-78 (1)
50-78: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winAdd route tests for the new run contract.
This endpoint adds important behavior with a few edge paths: malformed-but-valid JSON (
null), 409 when a run is already active, and the success path that writes cache. Please cover those under__tests__/so regressions in the rerun flow get caught early. As per coding guidelines, "Always add unit tests for new behaviour. Place tests in tests/. "🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/api/audit/run/route.ts` around lines 50 - 78, Add unit tests under __tests__/ for the POST route behavior in route.ts: cover the malformed-but-valid JSON case (body "null") returning 400, the concurrency case when tryAcquireRun() returns false producing a 409 with { status: "already-running" }, and the successful path where sanitize(body) is used, runAudit(opts) resolves and writeDashboardCache(opts, result) is called and the response is { status: "ok", result }; mock/stub tryAcquireRun, runAudit, writeDashboardCache, and releaseRun to assert they are invoked appropriately and that releaseRun() runs in finally.app/audit/_components/rerun-button.tsx-46-79 (1)
46-79:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't treat failed reruns as completed reruns.
triggerRun()resolves on POST failures, network failures, and poll timeout, so callers can't distinguish success from failure. InReturnSectionthat means a stale page reload still happens after a failed rerun. Return an explicit status or throw so the caller only runs its completion path when a scan actually finished.Proposed fix
-export async function triggerRun(scanParams: ScanParams): Promise<void> { +export async function triggerRun(scanParams: ScanParams): Promise<boolean> { try { const res = await fetch("/api/audit/run", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(paramsToBody(scanParams)), }); if (!res.ok && res.status !== 409) { const text = await res.text().catch(() => ""); console.error("audit run failed:", res.status, text); - return; + return false; } } catch (err) { console.error("audit run request failed:", err); - return; + return false; } const startedAt = Date.now(); while (Date.now() - startedAt < MAX_POLL_MS) { await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS)); try { const sres = await fetch("/api/audit/status", { cache: "no-store" }); if (!sres.ok) continue; const s = await sres.json() as { running: boolean }; - if (!s.running) return; + if (!s.running) return true; } catch { // Transient — keep polling. } } + + return false; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/audit/_components/rerun-button.tsx` around lines 46 - 79, triggerRun currently resolves on POST failures, network errors, and poll timeouts so callers (e.g., ReturnSection) can't tell success from failure; change triggerRun to throw on any POST non-OK (except allowed 409) and on network/fetch errors and to throw if polling exceeds MAX_POLL_MS without seeing running flip to false, and only resolve (return) when the poll observes the scan finished; include response text or error details in thrown Error messages (reference triggerRun, paramsToBody, /api/audit/run, /api/audit/status, MAX_POLL_MS, POLL_INTERVAL_MS) so callers can run completion logic only on true success.app/api/audit/run/route.ts-51-59 (1)
51-59:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReject non-object JSON bodies before calling
sanitize().
JSON.parse("null")succeeds, sobodybecomesnullhere andsanitize(body)throws on Line 59. That turns a bad request into a 500. Validate that the parsed payload is a plain object and return 400 otherwise.Proposed fix
export async function POST(request: NextRequest): Promise<NextResponse> { let body: RunBody = {}; try { const raw = await request.text(); - if (raw) body = JSON.parse(raw) as RunBody; + if (raw) { + const parsed: unknown = JSON.parse(raw); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); + } + body = parsed as RunBody; + } } catch { return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/api/audit/run/route.ts` around lines 51 - 59, The parsed JSON may be non-object (e.g., null or an array) which causes sanitize(body) to throw; after parsing the request text (the result of request.text() and JSON.parse), validate that the parsed value is a plain non-null object (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) before assigning to body and calling sanitize(body); if the check fails, return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }) so sanitize() only receives a proper object (refer to the RunBody variable, the local body/parsing logic, and the sanitize() call).app/api/audit/_state.ts-21-39 (1)
21-39:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftMove the run lock out of module memory.
app/api/audit/_state.tskeeps the guard in module-level in-memory variables, so it only coordinates within a single Node.js process/worker. If your deployment runs multiple replicas/processes,/api/audit/runcan acquire the lock in one worker while/api/audit/statuspolls another (or another run is accepted), makingrunning/concurrency incorrect. Use a shared/distributed lock+status store (e.g., Redis/DB with atomic acquire and TTL) instead of module memory.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/api/audit/_state.ts` around lines 21 - 39, The module-level lock represented by state and the functions tryAcquireRun, releaseRun, and getRunState must be replaced with a distributed lock/status stored in a shared system (e.g., Redis or DB): change tryAcquireRun to perform an atomic acquire (e.g., Redis SET key value NX EX ttl) and store a unique owner token and startedAt in the shared store returning true only on success, change releaseRun to release only if the owner token matches (to avoid deleting another process's lock), and change getRunState to read running and startedAt from the shared store; ensure TTL is used to avoid stuck locks and persist startedAt alongside the lock.app/audit/_components/run-progress.tsx-29-37 (1)
29-37:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep the progress UI below “complete” until the run actually finishes.
This animation hits
4/4and 100% after 16 seconds, but the copy says runs can take up to ~30 seconds. On slower audits the screen looks done while the request is still pending, which reads like a hang.💡 One way to avoid the false-complete state
export function RunProgress() { const [stage, setStage] = useState(0); const [tick, setTick] = useState(0); + const atLastStage = stage === STAGES.length - 1; + const progressPct = atLastStage ? 90 : ((stage + 1) / STAGES.length) * 100; useEffect(() => { @@ <div className="running-bar-label"> <span>progress</span> - <span style={{ color: "var(--dim)" }}>{stage + 1}/{STAGES.length}</span> + <span style={{ color: "var(--dim)" }}> + {atLastStage ? "finishing up" : `${stage + 1}/${STAGES.length}`} + </span> </div> <div className="running-bar-track"> <div className="running-bar-fill" - style={{ width: `${((stage + 1) / STAGES.length) * 100}%` }} + style={{ width: `${progressPct}%` }} /> </div>Also applies to: 88-96
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/audit/_components/run-progress.tsx` around lines 29 - 37, The progress UI currently advances through all STAGES based purely on STAGE_DURATION_MS inside the useEffect (timers updating setStage and setTick), causing it to reach the final "complete" stage before the audit actually finishes; modify the effect to cap stage progression at STAGES.length - 2 (or otherwise prevent reaching the final index) while the run is still pending, and only allow setStage to advance to STAGES.length - 1 when a real completion flag (e.g., an isFinished/isComplete prop or state tied to the audit request) is true; keep the existing tick timer behavior but ensure the stageTimer callback checks that completion flag before calling setStage((s) => Math.min(s + 1, STAGES.length - 1)) so the UI only shows 100% when the run has truly finished.app/audit/_components/policies-section.tsx-63-110 (1)
63-110: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winAdd unit coverage for this policy derivation.
This mapping/aggregation logic now defines the report output, but the supplied tests only cover cache/replay behavior. Please add cases for detector mapping, enabled-policy exclusion, and multi-source aggregation under
__tests__/audit/. As per coding guidelines, "Always add unit tests for new behaviour. Place tests in tests/."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/audit/_components/policies-section.tsx` around lines 63 - 110, Add unit tests for buildPolicyCards in a new test under __tests__/audit/ that exercise detector-to-primary-policy mapping, exclusion when a policy is already enabled, and aggregation from multiple sources: construct AuditResult objects with rows where source === "audit-detector" that map via DETECTOR_TO_PRIMARY_POLICY (use shortName(row.name) keys), rows with source === "builtin" and both enabledInConfig true/false to verify enabled ones are skipped, and multiple rows that target the same policy to confirm hits/projects/sources aggregate into one PolicyCard; assert the returned PolicyCard array (from buildPolicyCards) contains expected name, hits, projects, desc (from POLICY_DESC fallback), and that the catches string includes the via list when applicable. Ensure tests import buildPolicyCards, DETECTOR_TO_PRIMARY_POLICY, shortName and use enabledBuiltinNames in AuditResult to cover exclusion behavior.app/audit/_components/policies-section.tsx-152-178 (1)
152-178:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse the actual CLI binary in the copied install command.
This builds
failproof policy add …, but the rest of this PR surfaces the CLI asfailproofai. Copying the current string will hand users a command that doesn't match the shipped binary.🔧 Proposed fix
- const install = `failproof policy add ${policy.name}`; + const install = `failproofai policy add ${policy.name}`;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/audit/_components/policies-section.tsx` around lines 152 - 178, The install string in PolicyTile is using the wrong CLI binary name; update the install template in the PolicyTile component (where install is defined) from "failproof policy add ${policy.name}" to "failproofai policy add ${policy.name}" so the copied command matches the shipped binary, ensuring handleCopy still writes the updated install variable to the clipboard.assets/audit/poster.jsx-93-100 (1)
93-100:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPreserve the current poster params when linking back to the audit report.
Both back links drop the active
a/s/g/r/c/pquery string, so a poster opened directly or shared externally can't reconstruct the same audit view and falls back to defaults instead.🩹 Suggested direction
function Poster() { + const auditHref = "Audit Report.html" + window.location.search; const key = getParam("a", "optimist"); @@ - window.location.href = "Audit Report.html"; + window.location.href = auditHref; @@ - <a className="poster-back" href="Audit Report.html" onClick={handleBack}> + <a className="poster-back" href={auditHref} onClick={handleBack}> @@ - <a href="Audit Report.html" style={{ color: "var(--ink-2)" }}>view full audit →</a> + <a href={auditHref} style={{ color: "var(--ink-2)" }}>view full audit →</a>Also applies to: 107-107, 239-239
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@assets/audit/poster.jsx` around lines 93 - 100, The back-link handler handleBack currently navigates to "Audit Report.html" without preserving poster query params (a/s/g/r/c/p); update handleBack (and the other back-link handlers referenced) to capture the current window.location.search (or filter and rebuild a search string keeping only keys a, s, g, r, c, p) and append it to "Audit Report.html" (e.g., "Audit Report.html" + "?" + preservedSearch) before setting window.location.href, and ensure e.preventDefault() remains when redirecting.assets/audit/archetypes.jsx-246-266 (1)
246-266:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep the
Sigilfallback consistent.Line 247 falls back to
SIGILS.optimist, but Line 265 still readsARCHETYPES[archetypeKey].indexdirectly. Any unknownarchetypeKeywill still crash render even though the grid fallback succeeded.🩹 Proposed fix
function Sigil({ archetypeKey }) { - const grid = SIGILS[archetypeKey] || SIGILS.optimist; + const archetype = ARCHETYPES[archetypeKey] || ARCHETYPES.optimist; + const grid = SIGILS[archetype.key] || SIGILS.optimist; const cells = []; for (let y = 0; y < 8; y++) { const row = grid[y] || "........"; @@ <div className="sigil">{cells}</div> <div className="sigil-label"> - <span className="ix">№{ARCHETYPES[archetypeKey].index}</span> + <span className="ix">№{archetype.index}</span> sigil </div> </div> ); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@assets/audit/archetypes.jsx` around lines 246 - 266, The Sigil component uses SIGILS[archetypeKey] with a fallback but still accesses ARCHETYPES[archetypeKey].index directly which can crash for unknown keys; update Sigil to compute a single safe fallback (e.g., const archetype = ARCHETYPES[archetypeKey] || ARCHETYPES.optimist and const grid = SIGILS[archetypeKey] || SIGILS.optimist) and then use archetype.index and any archetype properties for the label instead of indexing ARCHETYPES again with archetypeKey so both grid and metadata consistently use the fallback.assets/audit/audit.jsx-18-26 (1)
18-26:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSanitize numeric query params before storing defaults.
Lines 20-22 accept
parseInt(...)results verbatim, so malformed?s=,?r=, or?c=values becomeNaNand then flow into score math,toLocaleString(), and rank/percentile copy across the page. This is user-reachable from the URL.🩹 Suggested direction
+function getIntParam(name, fallback, { min, max } = {}) { + const raw = Number.parseInt(getParam(name, String(fallback)), 10); + if (!Number.isFinite(raw)) return fallback; + const lower = min ?? raw; + const upper = max ?? raw; + return Math.min(upper, Math.max(lower, raw)); +} + const REPORT_DEFAULTS = /*EDITMODE-BEGIN*/{ "archetype": getParam("a", "optimist"), - "score": parseInt(getParam("s", "58"), 10), - "rank": parseInt(getParam("r", "1847"), 10), - "cohort": parseInt(getParam("c", "2316"), 10), + "score": getIntParam("s", 58, { min: 0, max: 100 }), + "rank": getIntParam("r", 1847, { min: 1 }), + "cohort": getIntParam("c", 2316, { min: 1 }), "tweetVariant": "show-off", "showSecondary": true, "project": getParam("p", "blrnow / api-coder") }/*EDITMODE-END*/;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@assets/audit/audit.jsx` around lines 18 - 26, REPORT_DEFAULTS currently assigns score, rank, and cohort using parseInt(getParam(...)) directly which can yield NaN from malformed query params; update the initialization in REPORT_DEFAULTS so that for each numeric field (score, rank, cohort) you parse the param (using parseInt(..., 10) as already done), then validate the result (Number.isFinite or !Number.isNaN) and if invalid fall back to the original hardcoded defaults (58, 1847, 2316 respectively); reference the REPORT_DEFAULTS object and getParam usages so the fallback logic is applied inline where those values are set.assets/audit/tweaks-panel.jsx-192-200 (1)
192-200:⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidate the
postMessagesender before accepting edit-mode commands.
assets/audit/tweaks-panel.jsxtoggles the panel based solely one.data.type(__activate_edit_mode/__deactivate_edit_mode) with noe.source/e.originchecks, so any frame can drive the host edit-mode UI. Gate on the expected sender at minimum (e.source === window.parent), and validatee.originif the embed origin is known.🩹 Proposed fix
React.useEffect(() => { const onMsg = (e) => { + if (e.source !== window.parent) return; const t = e?.data?.type; if (t === '__activate_edit_mode') setOpen(true); else if (t === '__deactivate_edit_mode') setOpen(false); };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@assets/audit/tweaks-panel.jsx` around lines 192 - 200, The message handler inside the React.useEffect (onMsg) currently toggles setOpen based only on e.data.type; update onMsg to first verify the sender by checking e.source === window.parent and, if you know the embed origin, also validate e.origin against that expected origin before acting on __activate_edit_mode / __deactivate_edit_mode; keep the window.parent.postMessage call but if an expected origin is available use it instead of '*' and ensure the cleanup still removes the same onMsg listener.assets/audit/poster.jsx-85-90 (1)
85-90:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFix async clipboard copy success state and preserve poster query params when navigating back
assets/audit/poster.jsx(lines 85-91):navigator.clipboard.writeText(...)is Promise-based; the currenttry/catchwon’t catch permission/insecure-context failures, yet the UI still flips to[ link copied ]. Make the handlerasyncand only setsetCopied(true)after a successfulawait, and handle failures.- `assets/audit/poster.jsx` (lines 93-100, link at 107, footer link at 239): navigating to `"Audit Report.html"` drops `window.location.search`, so the shared poster state (`a/s/g/r/c/p`) isn’t preserved. Append the current `window.location.search` to the destination (and reuse the same navigation logic for the footer link too).🩹 Proposed fix
- const handleCopyLink = () => { + const handleCopyLink = async () => { try { - navigator.clipboard.writeText(window.location.href); + await navigator.clipboard.writeText(window.location.href); setCopied(true); setTimeout(() => setCopied(false), 1600); - } catch (e) {} + } catch (e) { + setCopied(false); + } };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@assets/audit/poster.jsx` around lines 85 - 90, The handleCopyLink handler currently calls navigator.clipboard.writeText(...) without awaiting the Promise and flips UI state prematurely; make handleCopyLink async, await navigator.clipboard.writeText(window.location.href) inside a try/catch, call setCopied(true) only after a successful await and handle/log failures in the catch (and still avoid unhandled rejections), and keep the setTimeout to clear the state; additionally, when building navigation targets to "Audit Report.html" (the link at the in-file link and the footer link), preserve poster query params by appending window.location.search to the destination URL (reuse the same query-appending logic for both the inline link and the footer link so the shared poster state a/s/g/r/c/p is retained).
🧹 Nitpick comments (1)
app/audit/_components/policies-section.tsx (1)
28-39: ⚡ Quick winShare the detector→policy map instead of mirroring it.
Keeping a second hard-coded copy here can drift from
src/audit/findings.ts, which would make the Findings and Policies sections disagree for the same detector. Export the mapping from one place or move it into shared audit metadata.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/audit/_components/policies-section.tsx` around lines 28 - 39, The local DETECTOR_TO_PRIMARY_POLICY constant in policies-section.tsx is a duplicate of the mapping in src/audit/findings.ts (DETECTOR_TO_POLICY); remove the hard-coded copy and instead re-export or share the single source of truth: either export DETECTOR_TO_PRIMARY_POLICY (or alias DETECTOR_TO_POLICY) from findings.ts or move the mapping into a new shared audit metadata module and import it into policies-section.tsx and findings.ts; update the import in app/audit/_components/policies-section.tsx to use the shared export and delete the local const to avoid drift between Findings and Policies.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 64607466-755d-46c1-b890-734e8f797f75
⛔ Files ignored due to path filters (12)
app/icon.pngis excluded by!**/*.pngassets/audit/assets/fonts/architype-stedelijk.ttfis excluded by!**/*.ttfassets/audit/assets/fonts/architype-stedelijk.woff2is excluded by!**/*.woff2assets/audit/screenshots/poster-optimist.pngis excluded by!**/*.pngassets/audit/screenshots/poster-scrolled.pngis excluded by!**/*.pngassets/logos/company/icon.svgis excluded by!**/*.svgassets/logos/company/logo.svgis excluded by!**/*.svgbun.lockis excluded by!**/*.lockpublic/audit/fonts/architype-stedelijk.ttfis excluded by!**/*.ttfpublic/audit/fonts/architype-stedelijk.woff2is excluded by!**/*.woff2public/icon.svgis excluded by!**/*.svgpublic/logo.svgis excluded by!**/*.svg
📒 Files selected for processing (62)
CHANGELOG.md__tests__/audit/dashboard-cache.test.ts__tests__/audit/replay.test.tsapp/actions/get-audit-result.tsapp/api/audit/_state.tsapp/api/audit/run/route.tsapp/api/audit/status/route.tsapp/api/auth/login-request/route.tsapp/api/auth/login-verify/route.tsapp/api/auth/logout/route.tsapp/api/auth/reminder/route.tsapp/api/auth/status/route.tsapp/audit/_components/audit-dashboard.tsxapp/audit/_components/auth-dialog.tsxapp/audit/_components/empty-state.tsxapp/audit/_components/findings-section.tsxapp/audit/_components/identity-section.tsxapp/audit/_components/policies-section.tsxapp/audit/_components/report-footer.tsxapp/audit/_components/rerun-button.tsxapp/audit/_components/return-section.tsxapp/audit/_components/run-progress.tsxapp/audit/_components/score-section.tsxapp/audit/_components/show-off-cta.tsxapp/audit/_components/sigil.tsxapp/audit/_components/strengths-section.tsxapp/audit/audit-styles.cssapp/audit/loading.tsxapp/audit/page.tsxapp/globals.cssapp/layout.tsxapp/policies/hooks-client.tsxapp/projects/loading.tsxapp/projects/page.tsxassets/audit/Audit Report.htmlassets/audit/Show Off Your Agent.htmlassets/audit/archetypes.jsxassets/audit/audit.jsxassets/audit/poster-styles.cssassets/audit/poster.jsxassets/audit/styles.cssassets/audit/tweaks-panel.jsxbin/failproofai.mjscomponents/navbar.tsxcomponents/reach-developers.tsxdocs/cli/auth.mdxdocs/cli/environment-variables.mdxdocs/dashboard.mdxeslint.config.mjslib/auth/api-server-client.tslib/auth/auth-store.tspackage.jsonsrc/audit/archetypes.tssrc/audit/dashboard-cache.tssrc/audit/findings.tssrc/audit/index.tssrc/audit/replay.tssrc/audit/scoring.tssrc/audit/strengths.tssrc/audit/types.tssrc/auth/cli.tssrc/hooks/policy-registry.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/auth/cli.ts`:
- Around line 124-128: The ANSI color constants DIM, RESET, PINK, GREEN, and RED
are missing the ESC prefix so they print raw text; update each constant (DIM,
RESET, PINK, GREEN, RED in the cli.ts snippet) to include the ESC character
(e.g., "\x1b" or "\u001b") before the bracket so the sequences become "\x1b[2m",
"\x1b[0m", "\x1b[38;5;204m", etc., ensuring proper terminal formatting.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5a46601e-7489-4bf9-b4ef-04bb25463922
📒 Files selected for processing (4)
app/api/auth/status/route.tsapp/audit/_components/return-section.tsxbin/failproofai.mjssrc/auth/cli.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- app/audit/_components/return-section.tsx
|
❌ An unexpected error occurred while resolving merge conflicts: Not Found - https://docs.github.com/rest/git/refs#get-a-reference |
…heduler Add posthog tracking across login-request, login-verify, logout, and reminder routes plus the auth CLI; forward dashboard reminder set/clear to the api-server scheduler via new scheduleReminder/cancelReminder client helpers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…, sized to text via em, opacity+box-shadow dim/glow loop). - app/policies/hooks-client.tsx — removed the ━━ policies · live evaluation ● evaluating in real time masthead row; heading on the activity tab is now Policies + glowing dot. - app/projects/page.tsx and app/projects/loading.tsx — removed the ━━ projects · agent SDK folders ● N folders indexed row; heading is now Projects + glowing dot.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/audit/_components/return-section.tsx (1)
117-123:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle unauthorized reminder writes by reconciling local auth state.
persistReminder()treats all non-OK responses as a silentnulland keeps the current auth UI unchanged. On401, users can stay on the authed panel even though the session is gone, and “set a reminder” appears broken.Suggested patch
const persistReminder = useCallback(async (): Promise<Reminder | null> => { try { setReminderBusy(true); const res = await fetch("/api/auth/reminder", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ in_days: DEFAULT_REMINDER_DAYS }), }); - if (!res.ok) return null; + if (!res.ok) { + if (res.status === 401) { + setAuthStatus({ kind: "anon" }); + setReminder(null); + } + capture("audit_reminder_saved", { + status: `http_${res.status}`, + source: "return_section", + }); + return null; + } const body = (await res.json()) as { reminder?: Reminder }; capture("audit_reminder_saved", { status: body.reminder ? "success" : "empty", source: "return_section", }); return body.reminder ?? null;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/audit/_components/return-section.tsx` around lines 117 - 123, persistReminder() currently treats any non-OK response from fetch("/api/auth/reminder") as a silent null; update it to detect a 401 response and reconcile local auth state instead of leaving the UI in an authed state. Specifically, after performing the fetch with DEFAULT_REMINDER_DAYS, check res.status === 401 and in that branch invoke the existing session/ sign-out handler (e.g., call your signOut()/handleSignOut()/setAuthed(false)/revalidateSession function used elsewhere) to flip the UI back to unauthenticated; keep the existing behavior for other non-OK statuses (return null) and still parse body = await res.json() for OK responses. Ensure you reference the persistReminder() function and the fetch("/api/auth/reminder") call when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/auth/login-verify/route.ts`:
- Around line 45-50: The telemetry call uses raw PII via tokens.user.email;
change trackEvent usage in the code that calls
trackEvent("audit_user_identity_linked", ...) to stop sending the raw email —
either remove the user_email field or replace it with a non-reversible
identifier (e.g., SHA-256 hash of tokens.user.email or only the email domain)
computed before calling trackEvent; keep user_id (tokens.user.id) and
local_random_id (getInstanceId()) as-is and ensure the hashed/anonymized value
is named clearly (e.g., user_email_hash) so consumers know it is not raw PII.
- Line 41: The call to initTelemetry placed outside the guarded try block can
throw and convert a valid OTP verification into a 500; wrap the telemetry
initialization so it cannot bubble up—either move await initTelemetry() inside
the existing try that surrounds the login verification logic or call
initTelemetry() inside its own try/catch and swallow/log errors via the
telemetry/logger, ensuring the OTP verification code (the route handler)
continues executing even if initTelemetry fails.
In `@bin/failproofai.mjs`:
- Line 640: The closest-match suggestion list is missing the supported top-level
command "policy"; update the primary command array (const primary) to include
"policy" alongside the existing entries (e.g., "--version", "--help", "--hook",
"policies", "auth") so typo suggestions can match "policy" correctly.
In `@lib/auth/api-server-client.ts`:
- Around line 166-170: The cancelReminder function calls fetch without a
timeout, which can hang; modify cancelReminder to use an AbortController: create
a controller, start a timer (e.g. 5s or configurable) that calls
controller.abort(), pass controller.signal into
fetch(`${getApiBase()}/v0/reminders`, ...), and clear the timer after fetch
resolves; ensure any thrown AbortError is propagated/handled as appropriate so
callers of cancelReminder receive a timely failure instead of hanging.
---
Outside diff comments:
In `@app/audit/_components/return-section.tsx`:
- Around line 117-123: persistReminder() currently treats any non-OK response
from fetch("/api/auth/reminder") as a silent null; update it to detect a 401
response and reconcile local auth state instead of leaving the UI in an authed
state. Specifically, after performing the fetch with DEFAULT_REMINDER_DAYS,
check res.status === 401 and in that branch invoke the existing session/
sign-out handler (e.g., call your
signOut()/handleSignOut()/setAuthed(false)/revalidateSession function used
elsewhere) to flip the UI back to unauthenticated; keep the existing behavior
for other non-OK statuses (return null) and still parse body = await res.json()
for OK responses. Ensure you reference the persistReminder() function and the
fetch("/api/auth/reminder") call when making the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4014e35f-dc78-4190-a266-b0502440d871
📒 Files selected for processing (23)
CHANGELOG.mdapp/api/auth/login-request/route.tsapp/api/auth/login-verify/route.tsapp/api/auth/logout/route.tsapp/api/auth/reminder/route.tsapp/audit/_components/audit-dashboard.tsxapp/audit/_components/empty-state.tsxapp/audit/_components/findings-section.tsxapp/audit/_components/identity-section.tsxapp/audit/_components/policies-section.tsxapp/audit/_components/return-section.tsxapp/audit/_components/show-off-cta.tsxapp/globals.cssapp/policies/hooks-client.tsxapp/projects/loading.tsxapp/projects/page.tsxbin/failproofai.mjscomponents/navbar.tsxdocs/cli/audit.mdxlib/auth/api-server-client.tssrc/audit/index.tssrc/audit/telemetry.tssrc/auth/cli.ts
💤 Files with no reviewable changes (2)
- src/audit/telemetry.ts
- src/audit/index.ts
✅ Files skipped from review due to trivial changes (1)
- CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (10)
- app/api/auth/login-request/route.ts
- app/audit/_components/empty-state.tsx
- app/api/auth/logout/route.ts
- app/audit/_components/policies-section.tsx
- app/policies/hooks-client.tsx
- app/api/auth/reminder/route.ts
- app/globals.css
- src/auth/cli.ts
- app/audit/_components/identity-section.tsx
- app/audit/_components/show-off-cta.tsx
| { status: 400 }, | ||
| ); | ||
| } | ||
| await initTelemetry(); |
There was a problem hiding this comment.
Don’t let telemetry initialization break login verification.
Line 41 is outside the guarded try, so telemetry init errors can turn a valid OTP flow into a 500.
Suggested fix
- await initTelemetry();
+ try {
+ await initTelemetry();
+ } catch {
+ // best-effort telemetry: never block auth
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await initTelemetry(); | |
| try { | |
| await initTelemetry(); | |
| } catch { | |
| // best-effort telemetry: never block auth | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/auth/login-verify/route.ts` at line 41, The call to initTelemetry
placed outside the guarded try block can throw and convert a valid OTP
verification into a 500; wrap the telemetry initialization so it cannot bubble
up—either move await initTelemetry() inside the existing try that surrounds the
login verification logic or call initTelemetry() inside its own try/catch and
swallow/log errors via the telemetry/logger, ensuring the OTP verification code
(the route handler) continues executing even if initTelemetry fails.
| trackEvent("audit_user_identity_linked", { | ||
| source: "audit_set_reminder_auth_dialog", | ||
| user_id: tokens.user.id, | ||
| user_email: tokens.user.email, | ||
| local_random_id: getInstanceId(), | ||
| }); |
There was a problem hiding this comment.
Avoid sending raw user email in telemetry payloads.
Line 48 emits user_email to analytics. That’s direct PII and increases privacy/compliance exposure without being required for event correlation.
Suggested fix
trackEvent("audit_user_identity_linked", {
source: "audit_set_reminder_auth_dialog",
user_id: tokens.user.id,
- user_email: tokens.user.email,
local_random_id: getInstanceId(),
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| trackEvent("audit_user_identity_linked", { | |
| source: "audit_set_reminder_auth_dialog", | |
| user_id: tokens.user.id, | |
| user_email: tokens.user.email, | |
| local_random_id: getInstanceId(), | |
| }); | |
| trackEvent("audit_user_identity_linked", { | |
| source: "audit_set_reminder_auth_dialog", | |
| user_id: tokens.user.id, | |
| local_random_id: getInstanceId(), | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/auth/login-verify/route.ts` around lines 45 - 50, The telemetry call
uses raw PII via tokens.user.email; change trackEvent usage in the code that
calls trackEvent("audit_user_identity_linked", ...) to stop sending the raw
email — either remove the user_email field or replace it with a non-reversible
identifier (e.g., SHA-256 hash of tokens.user.email or only the email domain)
computed before calling trackEvent; keep user_id (tokens.user.id) and
local_random_id (getInstanceId()) as-is and ensure the hashed/anonymized value
is named clearly (e.g., user_email_hash) so consumers know it is not raw PII.
| } | ||
|
|
||
| const primary = ["--version", "--help", "--hook", "policies", "audit"]; | ||
| const primary = ["--version", "--help", "--hook", "policies", "auth"]; |
There was a problem hiding this comment.
Include policy in closest-match suggestions.
Line 640 omits a supported top-level command (policy), so typo suggestions can steer users to the wrong command.
Suggested fix
- const primary = ["--version", "--help", "--hook", "policies", "auth"];
+ const primary = ["--version", "--help", "--hook", "policies", "policy", "auth"];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const primary = ["--version", "--help", "--hook", "policies", "auth"]; | |
| const primary = ["--version", "--help", "--hook", "policies", "policy", "auth"]; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/failproofai.mjs` at line 640, The closest-match suggestion list is
missing the supported top-level command "policy"; update the primary command
array (const primary) to include "policy" alongside the existing entries (e.g.,
"--version", "--help", "--hook", "policies", "auth") so typo suggestions can
match "policy" correctly.
- auth-store: atomic temp-then-rename writes for auth.json / next-audit.json
with 0600 enforcement on both paths, so a crash mid-write can't truncate
the session or reminder file.
- api-server-client: 10s AbortSignal.timeout on every /v0/auth/* and
/v0/reminders fetch; surfaces as AuthApiError(code: "timeout").
- src/auth/cli.ts runLogin: treat stored auth.json as stale when its
refresh_expires_at has lapsed locally. Wipes + restarts OTP instead of
bouncing the user against a server-rejected session. Telemetry tagged
`replaced_stale: true`.
- /api/auth/reminder: distinguish empty body (defaults) from malformed
JSON / non-object body (400 validation_error) instead of silently
coercing to {} and writing a default-offset reminder.
- /api/audit/run: reject null / array / primitive bodies with 400 before
sanitize() throws 500.
- src/audit/dashboard-cache: add schemaVersion field; entries from older
shapes are rejected as null instead of being rendered against the
wrong shape.
- rerun-button: triggerRun now throws RerunError on POST failure /
network failure / poll-loop timeout; button paints a pink-border
"rerun failed — retry" state for 4s.
- run-progress: cap visual progress at 90% and swap last-stage detail to
"finishing up…" so a 30s run no longer paints 4/4 + 100% at 16s.
- identity-section: tighten LinkedIn-share "every key policy is live"
copy to grade === "A" && missing === 0.
- assets/audit/archetypes.jsx Sigil: normalize unknown archetypeKey once
so both SIGILS and ARCHETYPES.index use the same safe key.
- score-section: replace useMemo(pointsToNext) with plain
pointsToNextFor(score) — cheap 5-threshold scan, fixes
react-hooks/preserve-manual-memoization CI red.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…uth-store Add 40 tests across four previously-untested modules: - archetypes: zero-signal → precision, broad-spread → goldfish, secondary ≥40% promotion vs authored fallback, deterministic variant picker. - findings: ranking, zero-hit drop, detector→policy remapping, alsoCoveredBy, alreadyEnabled enable-set + builtin-config heuristics, relative-time + missing lastSeen fallback. - strengths: clean-rate headline, credential / retry / push-to-main absence gates, 5-item cap, fallback row when too few qualify. - auth-store: round-trip, mode 0600, atomic-write leaves no .tmp siblings, shape-mismatch rejection, reminder scoping, atomic overwrite. Full suite: 1741 passing (was 1701). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e events
Close two telemetry gaps surfaced during the audit/auth review:
1. CLI auth was emitting audit_otp_verified + audit_cli_auth_login_completed
with user_id on success, but never the audit_user_identity_linked event
that bridges the anonymous local instance ID to the server-issued user.
The dashboard's /api/auth/login-verify already emits this with
source: "audit_set_reminder_auth_dialog"; src/auth/cli.ts now emits the
CLI sibling with source: "cli", carrying user_id, user_email, and
local_random_id (getInstanceId()). Without it, anyone who signs in via
`failproofai auth login` stayed unjoined to their pre-auth events in
PostHog.
2. `failproofai policy add|remove` was emitting cli_policy_add_success /
cli_policy_remove_success on the happy path, but the failure path fell
through to the generic cli_parse_error / cli_unexpected_error events,
so dashboards couldn't compute add/remove conversion rates. bin/
failproofai.mjs now stashes the action in lastPolicyAction at dispatch
time and the top-level catch emits cli_policy_${action}_failure
(CliError or unexpected) — same shape as the existing
cli_install_failure / cli_uninstall_failure events.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ure events Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- audit_dashboard_viewed with score/grade/archetype/missing - audit_reminder_cta_shown / _clicked in return-section - audit_auth_dialog_opened / _dismissed / _succeeded with source tag - audit_rerun_failed with RerunError kind (rerun-button + return-section) - api_server_unreachable from fetchWithTimeout (kind, path, method) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DEFAULT_API_BASE flipped from http://localhost:8080 so published npm users hit the hosted api-server out of the box. CLI help text, unreachable-error copy, dashboard auth-dialog error string, and docs/cli/auth.mdx env-var table all updated to match. Local-dev contributors override with FAILPROOF_API_URL=http://localhost:8080. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The dedicated docs/cli/auth.mdx page already listed the default; the environment-variables index just said "override" without naming the fallback. Add it for parity. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ntry Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
src/audit/dashboard-cache.ts (1)
51-55:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReject
nullcache payloads during structural validation.The current guards accept
params: null/result: nullbecausetypeof nullis"object", so malformed cache can be treated as valid and crash later when fields are read.Suggested fix
- if ( - typeof entry?.cachedAt !== "string" - || typeof entry?.params !== "object" - || typeof entry?.result !== "object" - ) { + if ( + !entry + || typeof entry !== "object" + || typeof entry.cachedAt !== "string" + || !entry.params + || typeof entry.params !== "object" + || !entry.result + || typeof entry.result !== "object" + ) { return null; }Also applies to: 61-64
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/audit/dashboard-cache.ts` around lines 51 - 55, The structural validation in dashboard-cache.ts currently uses typeof checks that accept null (since typeof null === "object"); update the guard in the block that checks entry?.cachedAt, entry?.params, and entry?.result (and the similar guard at the other occurrence) to also explicitly reject null values for params and result — e.g. require typeof entry?.params === "object" AND entry?.params !== null, and likewise for entry?.result — so null cache payloads are treated as invalid and rejected during validation.app/audit/_components/rerun-button.tsx (1)
65-69:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd per-request timeouts for run and status fetches.
MAX_POLL_MSdoes not protect against a single hungfetch; one stalled request can block forever and prevent completion/failure handling.Suggested fix
+async function fetchWithTimeout(input: RequestInfo | URL, init: RequestInit = {}, timeoutMs = 15_000) { + const ac = new AbortController(); + const id = setTimeout(() => ac.abort(), timeoutMs); + try { + return await fetch(input, { ...init, signal: ac.signal }); + } finally { + clearTimeout(id); + } +} + export async function triggerRun(scanParams: ScanParams): Promise<void> { // Kick off the run. 409 (already running) is OK — we'll just poll. try { - const res = await fetch("/api/audit/run", { + const res = await fetchWithTimeout("/api/audit/run", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(paramsToBody(scanParams)), - }); + }); @@ - const sres = await fetch("/api/audit/status", { cache: "no-store" }); + const sres = await fetchWithTimeout("/api/audit/status", { cache: "no-store" });Also applies to: 83-90
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/audit/_components/rerun-button.tsx` around lines 65 - 69, Add a per-request timeout using AbortController for the POST to "/api/audit/run" (the fetch that sends JSON.stringify(paramsToBody(scanParams))) and for the subsequent status polling fetches so a single stalled request cannot hang the whole flow; create a REQUEST_TIMEOUT_MS constant, instantiate an AbortController for each fetch, start a setTimeout that calls controller.abort() after REQUEST_TIMEOUT_MS, pass controller.signal to fetch, and clear the timeout once fetch resolves; also catch and handle the abort error (treat as a timed-out request) in the same logic paths where the fetch results are processed.app/audit/_components/return-section.tsx (2)
61-360:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd tests for the return-loop state transitions.
This component now owns auth probing, focus/visibility rehydration, immediate post-auth reminder persistence, and rerun failure handling, but no
__tests__cover those branches. That is a lot of new stateful behavior to leave unpinned.As per coding guidelines, "Always add unit tests for new behaviour. Place tests in tests/."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/audit/_components/return-section.tsx` around lines 61 - 360, Add unit tests under __tests__ for the ReturnSection component that exercise the new auth and rehydration behaviors: mock fetch("/api/auth/status") and verify refreshStatus updates authStatus/reminder and that the focus/visibility handlers re-trigger it; test handleSetReminder flow for anon → dialog open and for authed → calls persistReminder and updates reminder; test handleAuthed calls persistReminder automatically and sets auth state; and test handleRerun failure path by mocking triggerRun to throw a RerunError (and a network error) and asserting capture is called with expected payloads. Target symbols: ReturnSection, refreshStatus, persistReminder, handleSetReminder, handleAuthed, handleRerun, and the AuthDialog interaction.
130-154:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReminder persistence can spin forever on a hung route.
persistReminder()flipsreminderBusybefore calling/api/auth/reminder, but there is no timeout or abort path. If that route stalls, the reminder CTA stays disabled indefinitely and the user has no recovery path in-page.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/audit/_components/return-section.tsx` around lines 130 - 154, persistReminder currently sets reminderBusy and calls fetch("/api/auth/reminder") with no timeout or abort, so a hung route leaves the UI disabled; wrap the fetch in an AbortController with a reasonable timeout (e.g. 5–10s) and clear the timer on success, and ensure the controller aborts on timeout so the catch/finally path runs and setReminderBusy(false) is always reached; update the persistReminder implementation (referencing persistReminder, setReminderBusy, and the fetch to "/api/auth/reminder") to create an AbortController, pass its signal to fetch, start a timeout to call controller.abort(), and clear that timeout in the success/finally branches.src/auth/cli.ts (1)
69-355:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd CLI tests for the new auth flow branches.
This lands new parsing, stale-session handling, OTP retry behavior, logout semantics, and
whoamiexit-code behavior without companion__tests__coverage. A small mocked suite aroundparseAuthArgs(), expired-session reauth, and logout/whoami would catch regressions quickly.As per coding guidelines, "Always add unit tests for new behaviour. Place tests in tests/."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/auth/cli.ts` around lines 69 - 355, Add unit tests under __tests__ to cover the new auth CLI behavior: write tests for parseAuthArgs to exercise flags, legacy flags and error paths; a runLogin test that mocks readAuth to return an expired session and asserts deleteAuth is called and the flow proceeds to prompt/OTP behavior (mock requestLoginCode/verifyLoginCode and trackHookEvent); a runLogout test that mocks readAuth and logoutSession to assert deleteAuth is always called and the "upstream" tracking value changes on revoke failure; and a runWhoami test that verifies process.exitCode is set to 1 when no session and prints the expected output. Use the functions parseAuthArgs, runLogin, runLogout, runWhoami and runAuthCli as the entry points and mock helpers like readAuth, deleteAuth, requestLoginCode, verifyLoginCode, logoutSession, and trackHookEvent to isolate behavior.app/audit/_components/auth-dialog.tsx (2)
120-159:⚠️ Potential issue | 🟠 Major | ⚡ Quick winThese requests can wedge the modal indefinitely.
Both flows set
busybeforefetch(), andbusydisables Escape, overlay-click, and the close button. Without a client-side timeout/abort, a hung/api/auth/login-requestor/api/auth/login-verifycall leaves the dialog stuck until a full page reload.Also applies to: 163-199
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/audit/_components/auth-dialog.tsx` around lines 120 - 159, requestCode currently sets busy before calling fetch and can leave the modal permanently disabled if the network request hangs; wrap the fetch in an AbortController with a client-side timeout (e.g. setTimeout to call controller.abort after N ms), pass controller.signal to fetch, clear the timeout on success, and handle the abort case in the catch (treat AbortError as a network-timeout and setStep({ kind: "email", error: "...timeout..." }) or similar) so busy is always cleared in finally. Make the exact same change to the verifyCode function so both requestCode and verifyCode use an AbortController + timeout, clear timers on completion, and ensure setBusy(false) runs in finally even when aborted.
44-382:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd unit tests for the auth-dialog state machine.
This introduces a new email → code → done flow, resend countdown, and open/dismiss/success telemetry, but no
__tests__coverage landed with it. Those branches are stateful enough that manual verification will miss regressions.As per coding guidelines, "Always add unit tests for new behaviour. Place tests in tests/."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/audit/_components/auth-dialog.tsx` around lines 44 - 382, The PR adds an email→code→done state machine in AuthDialog but no unit tests; add tests under __tests__ that mount AuthDialog and exercise the flows: submit email (triggering requestCode via onEmailSubmit), transition to "code" state and focus behavior, verify code (triggering verifyCode via onCodeSubmit) to reach "done", the resend countdown and onResend behavior (ensure resendIn ticks down and blocks until zero), and telemetry events (capture calls for audit_auth_dialog_opened, audit_auth_dialog_dismissed when closed without success, and audit_auth_dialog_succeeded when onAuthed is called); reference the component AuthDialog and handlers requestCode, verifyCode, onEmailSubmit, onCodeSubmit, onResend, and the step.kind transitions in your assertions, mocking fetch and the usePostHog capture to assert correct behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@__tests__/lib/api-server-client.test.ts`:
- Around line 15-72: Add unit tests in __tests__/lib/api-server-client.test.ts
that cover the new public behaviors: write tests for scheduleReminder() and
cancelReminder() to ensure reminders are scheduled and cancelled correctly
(including side effects/returned values), add tests that exercise parsing of the
Retry-After header (both seconds and HTTP-date forms) when fetch responses
return 429, and add tests for decodeJwt() including valid, expired, and
malformed tokens; use the existing requestLoginCode() tests as a template to
mock globalThis.fetch and assert telemetry (trackEventMock) and returned results
for these new paths so regressions in scheduleReminder, cancelReminder,
retry-after parsing, and decodeJwt are detected.
In `@__tests__/lib/auth-store.test.ts`:
- Around line 116-151: Add the same file-permission assertion for the reminder
file as exists for auth.json: after creating/writing a reminder (via
writeReminder or in the test that rounds-trips or immediately after
writeReminder), stat the file returned by getReminderFilePath() and assert its
mode is 0o600 (owner read/write only). Update the "reminder" describe block
tests (e.g., the "round-trips a written reminder" or a new test) to use
getReminderFilePath() and check the file's permissions so regressions on
next-audit.json are caught.
In `@src/auth/cli.ts`:
- Around line 273-278: Remove the PII by deleting the user_email field from the
telemetry payload sent to trackHookEvent in src/auth/cli.ts; keep the existing
keys (source, user_id, local_random_id) so the call to
trackHookEvent(getInstanceId(), "audit_user_identity_linked", { ... }) only
includes source, user_id, and local_random_id and does not reference
tokenResp.user.email.
---
Outside diff comments:
In `@app/audit/_components/auth-dialog.tsx`:
- Around line 120-159: requestCode currently sets busy before calling fetch and
can leave the modal permanently disabled if the network request hangs; wrap the
fetch in an AbortController with a client-side timeout (e.g. setTimeout to call
controller.abort after N ms), pass controller.signal to fetch, clear the timeout
on success, and handle the abort case in the catch (treat AbortError as a
network-timeout and setStep({ kind: "email", error: "...timeout..." }) or
similar) so busy is always cleared in finally. Make the exact same change to the
verifyCode function so both requestCode and verifyCode use an AbortController +
timeout, clear timers on completion, and ensure setBusy(false) runs in finally
even when aborted.
- Around line 44-382: The PR adds an email→code→done state machine in AuthDialog
but no unit tests; add tests under __tests__ that mount AuthDialog and exercise
the flows: submit email (triggering requestCode via onEmailSubmit), transition
to "code" state and focus behavior, verify code (triggering verifyCode via
onCodeSubmit) to reach "done", the resend countdown and onResend behavior
(ensure resendIn ticks down and blocks until zero), and telemetry events
(capture calls for audit_auth_dialog_opened, audit_auth_dialog_dismissed when
closed without success, and audit_auth_dialog_succeeded when onAuthed is
called); reference the component AuthDialog and handlers requestCode,
verifyCode, onEmailSubmit, onCodeSubmit, onResend, and the step.kind transitions
in your assertions, mocking fetch and the usePostHog capture to assert correct
behavior.
In `@app/audit/_components/rerun-button.tsx`:
- Around line 65-69: Add a per-request timeout using AbortController for the
POST to "/api/audit/run" (the fetch that sends
JSON.stringify(paramsToBody(scanParams))) and for the subsequent status polling
fetches so a single stalled request cannot hang the whole flow; create a
REQUEST_TIMEOUT_MS constant, instantiate an AbortController for each fetch,
start a setTimeout that calls controller.abort() after REQUEST_TIMEOUT_MS, pass
controller.signal to fetch, and clear the timeout once fetch resolves; also
catch and handle the abort error (treat as a timed-out request) in the same
logic paths where the fetch results are processed.
In `@app/audit/_components/return-section.tsx`:
- Around line 61-360: Add unit tests under __tests__ for the ReturnSection
component that exercise the new auth and rehydration behaviors: mock
fetch("/api/auth/status") and verify refreshStatus updates authStatus/reminder
and that the focus/visibility handlers re-trigger it; test handleSetReminder
flow for anon → dialog open and for authed → calls persistReminder and updates
reminder; test handleAuthed calls persistReminder automatically and sets auth
state; and test handleRerun failure path by mocking triggerRun to throw a
RerunError (and a network error) and asserting capture is called with expected
payloads. Target symbols: ReturnSection, refreshStatus, persistReminder,
handleSetReminder, handleAuthed, handleRerun, and the AuthDialog interaction.
- Around line 130-154: persistReminder currently sets reminderBusy and calls
fetch("/api/auth/reminder") with no timeout or abort, so a hung route leaves the
UI disabled; wrap the fetch in an AbortController with a reasonable timeout
(e.g. 5–10s) and clear the timer on success, and ensure the controller aborts on
timeout so the catch/finally path runs and setReminderBusy(false) is always
reached; update the persistReminder implementation (referencing persistReminder,
setReminderBusy, and the fetch to "/api/auth/reminder") to create an
AbortController, pass its signal to fetch, start a timeout to call
controller.abort(), and clear that timeout in the success/finally branches.
In `@src/audit/dashboard-cache.ts`:
- Around line 51-55: The structural validation in dashboard-cache.ts currently
uses typeof checks that accept null (since typeof null === "object"); update the
guard in the block that checks entry?.cachedAt, entry?.params, and entry?.result
(and the similar guard at the other occurrence) to also explicitly reject null
values for params and result — e.g. require typeof entry?.params === "object"
AND entry?.params !== null, and likewise for entry?.result — so null cache
payloads are treated as invalid and rejected during validation.
In `@src/auth/cli.ts`:
- Around line 69-355: Add unit tests under __tests__ to cover the new auth CLI
behavior: write tests for parseAuthArgs to exercise flags, legacy flags and
error paths; a runLogin test that mocks readAuth to return an expired session
and asserts deleteAuth is called and the flow proceeds to prompt/OTP behavior
(mock requestLoginCode/verifyLoginCode and trackHookEvent); a runLogout test
that mocks readAuth and logoutSession to assert deleteAuth is always called and
the "upstream" tracking value changes on revoke failure; and a runWhoami test
that verifies process.exitCode is set to 1 when no session and prints the
expected output. Use the functions parseAuthArgs, runLogin, runLogout, runWhoami
and runAuthCli as the entry points and mock helpers like readAuth, deleteAuth,
requestLoginCode, verifyLoginCode, logoutSession, and trackHookEvent to isolate
behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: cfcdbf74-475a-4904-8a98-9f39894a798e
📒 Files selected for processing (23)
CHANGELOG.md__tests__/audit/archetypes.test.ts__tests__/audit/findings.test.ts__tests__/audit/strengths.test.ts__tests__/lib/api-server-client.test.ts__tests__/lib/auth-store.test.tsapp/api/audit/run/route.tsapp/api/auth/reminder/route.tsapp/audit/_components/audit-dashboard.tsxapp/audit/_components/auth-dialog.tsxapp/audit/_components/identity-section.tsxapp/audit/_components/rerun-button.tsxapp/audit/_components/return-section.tsxapp/audit/_components/run-progress.tsxapp/audit/_components/score-section.tsxassets/audit/archetypes.jsxbin/failproofai.mjsdocs/cli/auth.mdxdocs/cli/environment-variables.mdxlib/auth/api-server-client.tslib/auth/auth-store.tssrc/audit/dashboard-cache.tssrc/auth/cli.ts
✅ Files skipped from review due to trivial changes (3)
- docs/cli/environment-variables.mdx
- docs/cli/auth.mdx
- CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (7)
- assets/audit/archetypes.jsx
- app/api/audit/run/route.ts
- app/audit/_components/run-progress.tsx
- app/audit/_components/score-section.tsx
- lib/auth/auth-store.ts
- app/audit/_components/identity-section.tsx
- app/api/auth/reminder/route.ts
| describe("api-server-client fetchWithTimeout telemetry", () => { | ||
| const originalFetch = globalThis.fetch; | ||
|
|
||
| beforeEach(() => { | ||
| trackEventMock.mockClear(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| globalThis.fetch = originalFetch; | ||
| }); | ||
|
|
||
| it("fires api_server_unreachable with kind=timeout on AbortError", async () => { | ||
| globalThis.fetch = vi.fn(async () => { | ||
| const err = new Error("aborted"); | ||
| err.name = "AbortError"; | ||
| throw err; | ||
| }) as unknown as typeof fetch; | ||
|
|
||
| await expect(requestLoginCode("a@b.co")).rejects.toBeInstanceOf(AuthApiError); | ||
| expect(trackEventMock).toHaveBeenCalledWith( | ||
| "api_server_unreachable", | ||
| expect.objectContaining({ | ||
| kind: "timeout", | ||
| path: "/v0/auth/login/request", | ||
| method: "POST", | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it("fires api_server_unreachable with kind=network on connection error", async () => { | ||
| globalThis.fetch = vi.fn(async () => { | ||
| throw new TypeError("fetch failed"); | ||
| }) as unknown as typeof fetch; | ||
|
|
||
| await expect(requestLoginCode("a@b.co")).rejects.toBeInstanceOf(TypeError); | ||
| expect(trackEventMock).toHaveBeenCalledWith( | ||
| "api_server_unreachable", | ||
| expect.objectContaining({ | ||
| kind: "network", | ||
| path: "/v0/auth/login/request", | ||
| method: "POST", | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it("does NOT fire api_server_unreachable on a successful response", async () => { | ||
| globalThis.fetch = vi.fn(async () => | ||
| new Response( | ||
| JSON.stringify({ status: "code_sent", expires_in: 600, resend_available_in: 30 }), | ||
| { status: 200, headers: { "content-type": "application/json" } }, | ||
| ), | ||
| ) as unknown as typeof fetch; | ||
|
|
||
| const out = await requestLoginCode("a@b.co"); | ||
| expect(out.status).toBe("code_sent"); | ||
| expect(trackEventMock).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Add coverage for the new reminder and parsing paths.
This suite only exercises the telemetry side effect on requestLoginCode(). The new public behaviors in scheduleReminder(), cancelReminder(), retry-after parsing, and decodeJwt() still have no regression coverage, so contract breaks there will ship silently.
As per coding guidelines, "Always add unit tests for new behaviour. Place tests in tests/."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@__tests__/lib/api-server-client.test.ts` around lines 15 - 72, Add unit tests
in __tests__/lib/api-server-client.test.ts that cover the new public behaviors:
write tests for scheduleReminder() and cancelReminder() to ensure reminders are
scheduled and cancelled correctly (including side effects/returned values), add
tests that exercise parsing of the Retry-After header (both seconds and
HTTP-date forms) when fetch responses return 429, and add tests for decodeJwt()
including valid, expired, and malformed tokens; use the existing
requestLoginCode() tests as a template to mock globalThis.fetch and assert
telemetry (trackEventMock) and returned results for these new paths so
regressions in scheduleReminder, cancelReminder, retry-after parsing, and
decodeJwt are detected.
| describe("reminder", () => { | ||
| it("returns null when no reminder file exists", () => { | ||
| expect(readReminder()).toBeNull(); | ||
| }); | ||
|
|
||
| it("round-trips a written reminder", () => { | ||
| const r = fakeReminder(); | ||
| writeReminder(r); | ||
| const out = readReminder(); | ||
| expect(out).toEqual(r); | ||
| }); | ||
|
|
||
| it("scopes by user_email — the consumer enforces this", () => { | ||
| writeReminder(fakeReminder({ user_email: "bob@example.com" })); | ||
| const out = readReminder(); | ||
| expect(out?.user_email).toBe("bob@example.com"); | ||
| }); | ||
|
|
||
| it("rejects shape mismatches as null", () => { | ||
| writeFileSync(getReminderFilePath(), JSON.stringify({ next_audit_at: "string" }), "utf-8"); | ||
| expect(readReminder()).toBeNull(); | ||
| }); | ||
|
|
||
| it("deleteReminder removes the file", () => { | ||
| writeReminder(fakeReminder()); | ||
| expect(existsSync(getReminderFilePath())).toBe(true); | ||
| deleteReminder(); | ||
| expect(existsSync(getReminderFilePath())).toBe(false); | ||
| }); | ||
|
|
||
| it("overwrites the existing reminder atomically", () => { | ||
| writeReminder(fakeReminder({ next_audit_at: 1 })); | ||
| writeReminder(fakeReminder({ next_audit_at: 2 })); | ||
| expect(readReminder()?.next_audit_at).toBe(2); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Assert restrictive perms on next-audit.json too.
The suite checks 0600 on auth.json, but the new reminder file also stores user email and is part of the same hardening story. Add the same mode assertion for getReminderFilePath() so a permission regression on next-audit.json is caught.
As per coding guidelines, "Always add unit tests for new behaviour. Place tests in tests/."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@__tests__/lib/auth-store.test.ts` around lines 116 - 151, Add the same
file-permission assertion for the reminder file as exists for auth.json: after
creating/writing a reminder (via writeReminder or in the test that rounds-trips
or immediately after writeReminder), stat the file returned by
getReminderFilePath() and assert its mode is 0o600 (owner read/write only).
Update the "reminder" describe block tests (e.g., the "round-trips a written
reminder" or a new test) to use getReminderFilePath() and check the file's
permissions so regressions on next-audit.json are caught.
| void trackHookEvent(getInstanceId(), "audit_user_identity_linked", { | ||
| source: "cli", | ||
| user_id: tokenResp.user.id, | ||
| user_email: tokenResp.user.email, | ||
| local_random_id: getInstanceId(), | ||
| }); |
There was a problem hiding this comment.
Drop raw email from the identity-link telemetry event.
local_random_id and user_id are already enough to stitch anonymous and authenticated sessions here. Adding user_email sends avoidable PII into analytics with no obvious need.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/auth/cli.ts` around lines 273 - 278, Remove the PII by deleting the
user_email field from the telemetry payload sent to trackHookEvent in
src/auth/cli.ts; keep the existing keys (source, user_id, local_random_id) so
the call to trackHookEvent(getInstanceId(), "audit_user_identity_linked", { ...
}) only includes source, user_id, and local_random_id and does not reference
tokenResp.user.email.
feat(audit+auth):
/auditdashboard, email-OTP auth, SES-backed re-audit reminders & unified design systemAt-a-glance
This branch lands the
0.0.11-beta.3cut. It is large because three interlocking features were designed together and ship as one coherent slice:/auditdashboard — turns existingfailproofai auditdata into a personality-driven, shareable report with archetype classifier, scoring, findings, prescribed policies, and a PNG poster export.failproofai auth …CLI subcommand + dashboardAuthDialog, wired end-to-end to the Rustfailproof-api-server(/v0/auth/*).~/.failproofai/next-audit.jsonmirrored to the api-server's new/v0/remindersscheduler, which delivers the nudge via SES in prod.Plus: a unified dashboard design system (the
/auditpixel-craft language promoted to/policies+/projects), full PostHog telemetry across the new surfaces, a hardening sweep, and +40 new tests (1701 → 1741 passing).Table of contents
/auditdashboard1 ·
/auditdashboardA server-rendered report at
/auditdriven by the existingfailproofai auditdata.1.1 — Classifier + scoring
src/audit/archetypes.ts— 8 archetypes (optimist,cowboy,explorer,goldfish,paranoid architect,precision builder,hammer,ghost).SIGNAL_MAPcovers 47/47 builtin policies + audit-only detectors with tuned weights. Falls back togoldfishon broad-spread signal,precisionon zero signal.src/audit/scoring.ts— 0–100 score, S/A/B/C/D/F bands,projectedScore(uplift if every recommended policy were enabled),syntheticRankfor a stable cohort rank.src/audit/strengths.ts+src/audit/findings.ts— derive Strengths (real numbers: clean-call %, avg turns, "0 credential leaks" gated on the relevant sanitize policies firing) and per-policy Findings (hand-curated body + cost copy + real captured evidence fromAuditCount.examples).findings.ts:DETECTOR_TO_POLICY— every audit-only detector is paired with its closest real-time builtin policy, so every finding card carries a real$ failproofai policy add <slug>install command. Multi-policy mappings render an "also covered by<policy>" hint. The Prescribed-policies section aggregates detector hits into the target policy with(via redundant-cd-cwd, …)attribution.1.2 — Multi-variant persona catalog (
9a0b22b)pickArchetypeVariant(key, seed)— deterministic per-field djb2 hash mixed with a per-field axis. Persona blurb is stable for a given project seed but two different projects landing on the same archetype see different copy.undefinedkeywords) by forcing>>> 0on the final mix.IdentitySectionconsumes the resolved variant; the seed flows in fromaudit-dashboard.tsxas the inferred project name.1.3 — UI sections (
app/audit/_components/*)1.4 — Cache + API
~/.failproofai/audit-dashboard.json— mode0600, single-slot (new runs overwrite). Helper atsrc/audit/dashboard-cache.ts.AuditResultschema → version 2 — new fieldseventsScanned: number(drives the "X tool calls" headline),projectsScanned: string[](drives the project filter),enabledBuiltinNames: string[](lets findings answer "is this fix already enabled?" without iterating result rows).POST /api/audit/run— callsrunAudit()in-process, writes the cache, module-scoped run lock that 409s on concurrent clicks.GET /api/audit/status—{running, startedAt, cachedAt}for client polling at 1 Hz.app/actions/get-audit-result.ts— read-only server action; mirrors/policies'getHooksConfigAction()pattern.1.5 — Re-run UX + shareable poster
run-progress.tsx).RerunButtonPOSTs/api/audit/runwith the current scan params, polls/api/audit/statusat 1 Hz, and re-fetches via the server action whenrunningflips false.[ make poster ]captures the archetype frame viahtml2canvas@^1.4.1at scale 2 and downloadsfailproofai-<archetype>-<YYYY-MM-DD>.png.9a0b22b:await document.fonts.readybefore capture, a.capturingclass locks everyclamp()font-size and grid column to absolute values tuned for the 1100 px capture width, drops unpredictabletext-shadow/box-shadow, captures with a 12 px bleed so the frame's corner accents survive the crop.[ share → ]uses manualwindow.scrollTominus the sticky.app-headerheight (+ ascroll-margin-top: 80pxfallback on.showoff).1.6 — Runtime safety: snapshot/restore around
runAudit()src/hooks/policy-registry.tsgains two additive exports (getAllPolicies/setAllPolicies). They are used only byreplay.ts:restoreReplay()to snapshot/restore policy state, so embeddingrunAudit()in a long-running process (the dashboard API route) can no longer wipe pre-existing registrations.1.7 — Page gate
auditappended toFAILPROOFAI_DISABLE_PAGES's valid-values list; the page-level disable gate inapp/audit/page.tsxalready honored it.2 · Email-OTP authentication
End-to-end auth against the Rust
failproof-api-server(/v0/auth/login/request,/login/verify,/token/refresh,/logout,/me), exposed on both CLI and dashboard, with a single shared on-disk session file.2.1 — CLI surface (
src/auth/cli.ts, dispatched frombin/failproofai.mjs)Readline OTP masking is TTY-gated so piped stdin (tests / scripts) doesn't stall on the per-character
_writeToOutputcallback.2.2 — Shared store —
lib/auth/(used by both surfaces)api-server-client.ts— stateless fetch client. Endpoint helpers (requestLoginCode,verifyLoginCode,refreshAccessToken,logoutSession,fetchMe,decodeJwt).AuthApiErrorcarriesstatus,code,retry_after_secs. Tolerates both the documented{code, message}error shape and the live server's{success, code, detail}shape.auth-store.ts— file persistence at~/.failproofai/auth.json, mode0600(creation + post-writechmodSyncon overwrite).getValidAccessToken()auto-refreshes within a 60 s leeway;whoAmI()does one refresh-and-retry on a hard 401 then wipes the file.FAILPROOFAI_AUTH_DIRenv override exists for tests.2.3 — Dashboard routes —
app/api/auth/GET /status—{authenticated, user?, reminder?}viawhoAmI().POST /login-request— proxy; surfacesretry_after_secs.POST /login-verify— on 200 persists tokens locally and returns only{authenticated, user}. The refresh token never reaches the browser.POST /logout— revokes upstream, deletesauth.jsonregardless of upstream success.GET / POST / DELETE /reminder— see §3.2.4 — Dashboard UI
auth-dialog.tsx— modal matched to/audit's pixel-craft aesthetic: pink corner brackets, dashed-frame backdrop, terminal mono inputs, masked OTP entry, live 30 s resend countdown, ESC / backdrop /[x]close, rate-limit error banner.return-section.tsx— probes/api/auth/statuson mount and gates[ set a reminder ]on auth state (full behavior matrix in §3.3).2.5 — Default api-server URL flipped (
d93392e)lib/auth/api-server-client.ts:DEFAULT_API_BASEflipped fromhttp://localhost:8080→https://api.befailproof.aiso published npm users hit the hosted server out of the box. Local-dev contributors and self-hosted users continue to override viaFAILPROOF_API_URL=…. Updated copy lives in: CLI help text, the "could not reach"CliError,auth-dialog.tsx's error banner ("is it running on :8080?" → "check your network"),docs/cli/auth.mdx's env-var table, anddocs/cli/environment-variables.mdx's Authentication row.2.6 — Stale-session hardening (
507f3c9)runLogin()now treatsauth.jsonas stale when its localrefresh_expires_atclaim has lapsed — wipes the file and walks the OTP flow again instead of bouncing the user with "already signed in" against a file the server would reject. Telemetry taggedreplaced_stale: trueon the resulting_login_startedevent.3 · Persistent re-audit reminders
3.1 — Local state
~/.failproofai/next-audit.json— mode0600. Separate file fromauth.jsonso token refresh / re-login does not churn the reminder.New helpers in
lib/auth/auth-store.ts:readReminder·writeReminder·deleteReminder·getReminderFilePath·StoredRemindertype.3.2 — Dashboard route —
app/api/auth/reminder/route.tsGET— returns the current reminder for the active session. Per-email scoped — a reminder fora@x.comis invisible whenb@x.comis the live CLI session.POST— defaults to a 7-day offset. Empty body → defaults. Malformed JSON / non-object body → 400validation_error(hardened in507f3c9).DELETE— clears it./api/auth/statusnow returnsreminder: { next_audit_at, user_email, set_at } | nullalongside the user.3.3 — Behavior matrix —
return-section.tsxunknown(status in flight) — buttons disabled.anon—[ set a reminder ]opensAuthDialog; on successful sign-in the dialog auto-persists the 7-day reminder (no second click required).authed + no reminder—[ set a reminder ]writes the timestamp directly, no dialog.authed + reminder set— status panel showing:next audit set for <Mon Jun 8> · in 7 dayssigned in as <email>[ re-audit now ]+[ install policies ]+ tinyclear reminderlink.[ re-audit now ]is also exposed to anon users with audit data. It reusestriggerRun()fromrerun-button.tsxand reloads the page when the new run finishes.3.4 — Server-side delivery —
feat/ses-mailing(95a8e0c)POST / DELETE / GET /v0/remindersbacked byArc<Mutex<HashMap<email, Reminder>>>— one entry per email; re-setting replaces the old reminder.FAILPROOF_REMINDER_SWEEP_INTERVAL_SECSand delivers due reminders via the existingEmailSenderseam (SES in prod, log sender in dev).send_audit_reminder()added toEmailSenderwith a dedicated template; bothLogEmailSenderandSesEmailSenderimplement it. Test recorder intests/auth_flow.rsupdated to match.scheduleReminder/cancelReminderinlib/auth/api-server-client.tsforward the dashboard's POST/DELETE/api/auth/reminderto the api-server. Upstream failure is captured into telemetry asupstream: "failed"+upstream_errorbut does NOT fail the request — local state remains the source of truth for the UI.4 · Unified pixel-craft design system
Promotes the
/auditpage's brutalist pixel-craft design language to the whole app and fixes a latent bug where the audit page's:root+ body resets persisted on client-side navigation back to other routes.4.1 — Single source of truth →
app/globals.cssThe audit token set (
--bg,--ink,--accent-pink,--accent-green,--font-mono= JetBrains Mono,--font-display= Architype Stedelijk / VT323) is declared once. Every shadcn-style Tailwind alias (--background,--card,--foreground,--primary,--border,--radius: 0, …) is repointed at the audit palette — so existing utility classes likebg-card text-foreground border-borderproduce audit visuals everywhere without component rewrites.4.2 — No more style leak
app/audit/audit-styles.cssis trimmed by 150 lines — it keeps only audit-page-only widgets (archetype frame, sigil, score grade, findings cards, return hook, auth dialog). The:root,html/body/#rootresets, body atmosphere overlays,.app-header,.btn,.tabsall now live inglobals.css. Side effect: nothing is left to leak.4.3 — Component-level changes
app/layout.tsx— dropsnext/font/googleGeist Mono. Fonts ship via the single CSS@importinglobals.css.components/navbar.tsx— rewritten around.app-header: pink▮▮pixel mark, lowercase Architype wordmark, optional version chip, per-section eyebrow,.tablinks with sharp pink underline on active. Lucide icons removed. Audit entry added between Policies and Projects with a slipping-through count chip./policies+/projects— wrapped in.report+.section+.panelchrome with audit-style mastheads. InnerProjectList/ActivityTab/PoliciesTabare unchanged and pick up the unified palette automatically.4.4 — Sizing (
4e0f805)13px → 14.5px.reportmax-width1180 → 1380px(40 px side padding).sectionvertical padding restored to64pxDefault-zoom readability across
/audit,/policies,/projectsno longer forces a browser zoom-in.4.5 — Removed
app/audit/_components/app-header.tsx(4e0f805)The second in-page
failproof_ai / AUDIT [ share → ]strip is gone, plus its three mount sites inaudit-dashboard.tsx(cached, in-flight, ShellEmpty). The global navbar + per-section mastheads cover the same chrome without the duplicate.4.6 — Score section rewrite (
4e0f805)Drops the synthetic cohort leaderboard. New
.score-share-cardis a single dashed-frame.panelsplit into two columns:score + archetype + missing-count;[ share on X ],[ share on LinkedIn ], and[ download audit card ](html2canvas captures the whole panel asfailproofai-card-<grade>-<score>.png).audit-dashboard.tsxdrops the now-unusedsyntheticRankimport /rankprop and threadsresultinto the new section.4.7 — Empty / running views (
4e0f805)empty-state.tsx→.empty-panelwith a 6×6 pixel-grid sigil, Architype Stedelijk headline,.btn-pressCTA, audit-style meta caption. Replaces the prior shadcnButton+lucide-reacticon center-card.no-cache→ "run your first audit." with[ run audit ].zero-sessions→ "install hooks first." with[ install guide → ].run-progress.tsx→ terminal-style.running-panel:$ failproofai audit --since 30d ▮header with a blinking pink cursor; stage list with✓/▮▮/○markers + per-stage braille spinner; marquee progress bar with a pink shine sweep.4.8 — Glowing-dot section headings (
4bcd6eb).section-h-dot+ pulse keyframes (green dot, sized to text viaem, opacity + box-shadow dim/glow loop). Replaces the━━ projects · agent SDK folders ● N folders indexedand━━ policies · live evaluation ● evaluating in real timemasthead rows on/projectsand/policies— heading is nowProjects/Policies+ glowing dot.5 · Telemetry (PostHog)
PostHog instrumentation added across the new surfaces. The existing
FAILPROOFAI_TELEMETRY_DISABLED=1kill switch is honored by every new event.5.1 — Auth + reminder routes (
75eefec)app/api/auth/{login-request, login-verify, logout, reminder}/route.tsnow emit:audit_otp_requested·audit_otp_verified·audit_user_logged_outaudit_reminder_set·audit_reminder_clearedstatus+error_codeon failure. The OTP-request path carries a SHA-256 email hash so distinct senders are countable without storing PII.src/auth/cli.tsmirrors the same OTP/login/logout events plus CLI-only:audit_cli_auth_login_started·_login_completed·_logout_completed·_whoamihad_session/upstreamoutcomes.Also restores literal
\x1b[escape sequences in the CLI color constants that had been collapsed to raw control bytes in the prior commit.5.2 — Identity link + policy add/remove failures (
7fa1de9)audit_user_identity_linkedon CLI auth.src/auth/cli.tsnow emits the event on successful OTP verify withsource: "cli", carryinguser_id,user_email, andlocal_random_id=getInstanceId(). The dashboard's/api/auth/login-verifyalready emitted the same event withsource: "audit_set_reminder_auth_dialog"; this is the CLI sibling — without it, anyone who signed in viafailproofai auth --loginstayed unjoined to their pre-auth instance events in PostHog.cli_policy_{add,remove}_failureevents.bin/failproofai.mjsstashes the action inlastPolicyActionat dispatch time; the top-level catch emitscli_policy_${action}_failure(CliError or unexpected) witherror_type+exit_code— same shape as the existingcli_install_failure/cli_uninstall_failureevents. Enables add/remove conversion-rate analysis.5.3 — Five audit-page funnel gaps closed (
78c326e)audit_dashboard_viewed— fires when the report renders, carries{score, grade, archetype, secondary, missing, transcripts_scanned, results_count, detectors_triggered}. The existingaudit_page_viewedonly carriedstate+has_cache— click-through against share / download / rerun was impossible to compute without joining server logs.audit_reminder_cta_shown/audit_reminder_cta_clicked— fired inreturn-section.tsxafter the/api/auth/statusprobe resolves and on press. Closes the front of the funnelshown → clicked → AuthDialog → reminder_set.auth-dialog.tsxemitsaudit_auth_dialog_opened,_dismissed(with the step the user gave up on),_succeeded— all tagged with asourceprop ("return_section"). Exact dropoff at email vs OTP step is now visible.audit_rerun_failed— fires from bothrerun-button.tsxandreturn-section.tsx'shandleRerunwithkind(post_failed|network|timeout) fromRerunError. Alertable on rerun reliability without parsing/api/audit/runserver logs.api_server_unreachable— incremented fromfetchWithTimeoutinlib/auth/api-server-client.ts(kind + request path + method). "The api-server is down" is one PostHog count instead of a server-log scrape. No-op on the CLI side when telemetry has not been initialised.5.4 — Product tag
Every PostHog event across all four channels (hooks/audit, server, web UI, npm install/uninstall) is now stamped with
product: "failproofai-oss". The value lives in a singlePOSTHOG_PRODUCTconstant insrc/posthog-key.ts, reused by the three TypeScript channels; the standalonescripts/install-telemetry.mjsinlines the same literal because it cannot import the TS module at install time.6 · Hardening sweep
auth.jsonandnext-audit.jsonnow use temp-file-then-rename with0600enforced on both temp and final paths. A concurrent write or crash mid-write can no longer leave a half-written / truncated session or reminder file behind.AbortSignal.timeouton every/v0/auth/*and/v0/remindersfetch. A wedged DNS resolver, a hung api-server, or a stalled refresh no longer pins the CLI or a dashboard request indefinitely. Surfaces asAuthApiError(code: "timeout")— same "could not reach the api-server" copy as transport failures.runLoginstale-session handling — see §2.6./api/auth/reminder— empty body (defaults to 7-day offset) vs malformed JSON / non-object body (400validation_error) are now distinguished. Previously the route silently coerced to{}and wrote a default-offset reminder./api/audit/run— rejectsnull, arrays, and primitive bodies with 400 beforesanitize(null)throws 500. Guards both theJSON.parsepath and the post-parse shape check.dashboard-cache.ts— adds explicitschemaVersionfield. Entries written by older code are now rejected asnullinstead of being rendered against the wrong shape.rerun-button.tsx—triggerRunnow throwsRerunErroron POST failure / network failure / poll-loop timeout. Button paints a pink-borderrerun failed — retrystate for 4 s instead of pretending the run completed.triggerRunis typed asPromise<void>that explicitly throws soEmptyState's CTA can adopt the same UX.run-progress.tsx— caps visual progress at 90 % and swaps the last-stage detail tofinishing up…. A real 30 s run no longer paints 4/4 + 100 % at the 16 s mark.identity-section.tsx— LinkedIn-shareevery key policy is livecopy now requiresgrade === "A" && missing === 0(previously any A-grade triggered the verbatim copy even when there were unenabled prescribed policies).assets/audit/archetypes.jsx Sigil()— normalizes unknownarchetypeKeyonce at the top so the index lookup uses the same safe key as the sigil grid (previously the sigil lookup had a fallback butARCHETYPES[archetypeKey].indexcrashed on unknown keys).score-section.tsx—useMemo(pointsToNext)replaced with a plainpointsToNextFor(score)(cheap 5-threshold scan). Fixesreact-hooks/preserve-manual-memoizationCI red.7 · Tests
__tests__/audit/archetypes.test.ts— zero-signal →precision, broad-spread →goldfish, secondary ≥ 40 % promotion vs authored fallback, deterministic variant picker.__tests__/audit/findings.test.ts— ranking, zero-hit drop, detector → policy remapping,alsoCoveredBy,alreadyEnabledenable-set + builtin-config heuristics, relative-time + missinglastSeenfallback.__tests__/audit/strengths.test.ts— clean-rate headline, credential / retry / push-to-main absence gates, 5-item cap, fallback row when too few qualify.__tests__/lib/auth-store.test.ts— round-trip, mode0600, atomic-write leaves no.tmpsiblings, shape-mismatch rejection, reminder scoping, atomic overwrite.__tests__/audit/replay.test.ts— updated to cover the new snapshot/restore flow.__tests__/audit/dashboard-cache.test.tsand__tests__/lib/api-server-client.test.tsadded.8 · Docs
docs/cli/auth.mdx(new) — usage block;--login/--logout/--whoamisections; on-diskauth.jsonshape; env-var table; troubleshooting list for the commonCould not reach the api-server/Rate limited/Code rejectedcases. Persistent-reminder section covering~/.failproofai/next-audit.json(file shape, per-email scoping, 7-day default,0600perms) and theGET / POST / DELETE /api/auth/reminderendpoint.docs/cli/environment-variables.mdx— new Authentication section:FAILPROOF_API_URL(default now named:https://api.befailproof.ai) andFAILPROOFAI_AUTH_DIR(default~/.failproofai).docs/dashboard.mdx— new### Auditpage entry;auditappended toFAILPROOFAI_DISABLE_PAGESvalid values.CHANGELOG.md— full0.0.11-beta.3block under today's date.translate-docssync workflow picks them up.9 · Environment variables
9.1 — Dashboard / CLI (this repo)
FAILPROOF_API_URL· defaulthttps://api.befailproof.ai(flipped fromhttp://localhost:8080ind93392e)Base URL of the api-server. Override to
http://localhost:8080for local dev or to a self-hosted host.FAILPROOFAI_AUTH_DIR· default~/.failproofaiDirectory where
auth.json+next-audit.jsonlive. Tests use this to redirect to a tmpdir.FAILPROOFAI_DISABLE_PAGES· unsetComma-separated list of dashboard pages to disable. Now accepts
auditin addition to the existing values.FAILPROOFAI_TELEMETRY_DISABLED· unsetExisting kill switch. Honored by every new PostHog event added in this branch.
9.2 — Api-server (
feat/ses-mailing)FAILPROOF_REMINDER_SWEEP_INTERVAL_SECS· default60How often the background tokio task scans the in-memory map for due reminders.
FAILPROOF_REMINDER_DEFAULT_DAYS· default7Offset for the auto-scheduled-on-verify reminder.
FAILPROOF_REMINDER_AUTO_SCHEDULE_ON_VERIFY· defaulttrueWhen
true, registers a default 7-day reminder on every successful OTP verify so users get a baseline cadence without touching the dashboard.10 · Deployment notes
scheduleReminder/cancelReminderhelpers call/v0/reminders, which does not exist on api-servermain. Untilfeat/ses-mailingships, the dashboard succeeds locally (file write) and logsupstream: "failed"telemetry — no user-visible failure, but no mail goes out.DEFAULT_API_BASEflip. Published npm users now hithttps://api.befailproof.aiautomatically. Ensure that host is up and reachable at merge time. Self-hosted users continue to override viaFAILPROOF_API_URL(no breaking change for anyone who already had the env var set).rotated_tochain). The client treats any 401-from-refresh as "wipe local session," so theft-revoked users get pushed back to the login dialog automatically.bun run devonce after pulling — Turbopack HMR cannot hot-swap:root/@importchanges inglobals.cssreliably.11 · Verification
bunx tsc --noEmit— passesbun run lint— passes (only the two pre-existing warnings)bun run test:run— 1741 / 1741 passbun --bun next build— Compiled successfully; new/api/auth/{status, login-request, login-verify, logout, reminder}and/api/audit/{run, status}routes register;/auditpage builds.feat/ses-mailing:--login+--whoami+--logout/auditrendering with the gated reminder buttonauth.jsonacross both surfaces (sign in via CLI → dashboard sees it; logout via CLI → dashboard reverts to anonymous on next page load)12 · Test plan
feat/ses-mailingwithFAILPROOF_EMAIL_SENDER_BACKEND=logFAILPROOF_API_URL=http://127.0.0.1:8080 bun run devbun bin/failproofai.mjs auth --login→ OTP visible in api-server stdout → verify →--whoamiconfirms/audit→ empty state → run audit → report renders with archetype + score[ make poster ]downloads a PNG;[ download audit card ]downloads the score-share PNG[ set a reminder ]→ AuthDialog → sign in → reminder auto-set, no second click[ set a reminder ]writes directly, no dialog[ re-audit now ]triggers a fresh run + page reloadfailproofai auth --logout→ dashboard reverts to anon on next page loadaudit_dashboard_viewedaudit_reminder_cta_shown/_clicked/audit_reminder_setaudit_user_identity_linkedfrom both CLI and dashboardcli_policy_add_failure(force a bad slug)Commit map (most recent → oldest)