Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a3630a2
Gather the audit's files under audit/, as layout 4
SiddarthAA Aug 14, 2026
99a7c7c
Point the audit-lane e2e tests at the layout-4 schedule path
SiddarthAA Aug 14, 2026
42a78d9
Resume the CTA that opened the sign-in dialog, not always the reminder
SiddarthAA Aug 14, 2026
dc5581e
Report a scheduled audit's harmful findings, so the machine can tell you
SiddarthAA Aug 14, 2026
71af070
Merge the scheduled-audit controls into the audit page, delete /settings
SiddarthAA Aug 14, 2026
069c19b
Bound a first digest to one interval, and mask secrets that arrive cut
SiddarthAA Aug 14, 2026
9b35c7a
Give scheduled audits their own page, and let the audit be a report
SiddarthAA Aug 14, 2026
345a3d4
Stop a redaction test asserting a path shape only true on my machine
SiddarthAA Aug 14, 2026
5ff4434
feat(audit): schedule audits from the CLI, with terminal sign-in
SiddarthAA Aug 14, 2026
a906ba4
feat(settings): rebuild /settings as a console for the service
SiddarthAA Aug 14, 2026
3c8ced1
fix(ui): one colour per section eyebrow; settings masthead
SiddarthAA Aug 14, 2026
7a99771
fix(settings): offer the next step when a session is already dead
SiddarthAA Aug 14, 2026
7fb1488
Stop the redactor printing the username, and three more from review
SiddarthAA Aug 14, 2026
5d9259c
Five defects from an adversarial review pass
SiddarthAA Aug 14, 2026
b47fcac
Give the schedule CLI's sign-in the frame the wizard uses
SiddarthAA Aug 15, 2026
280e835
Answer the email question up front with --schedule --email
SiddarthAA Aug 15, 2026
ce3852c
Stop the layout-4 changelog contradicting itself
SiddarthAA Aug 15, 2026
93a66ee
Stop the digest shipping assigned secrets, and two redaction misfires
NiveditJain Aug 15, 2026
087144d
Do not read an old `audit.auto` as consent to send findings off the m…
NiveditJain Aug 15, 2026
c951d10
Warn about the daemon on the command that strands it
NiveditJain Aug 15, 2026
61cc76e
Four more from the review: a stranded token, a dead session, two claims
NiveditJain Aug 15, 2026
2695960
Bring the docs to what shipped, and re-gate /settings
NiveditJain Aug 15, 2026
2c7bcf5
Seven follow-ups from the review
NiveditJain Aug 15, 2026
22b4bd9
Close the test gaps, and stop the changelog contradicting itself
NiveditJain Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,13 @@ Run through this mentally after any change to `src/hooks/` or `dist/` build:
| `src/hooks/custom-hooks-loader.ts` | Top-level custom hook loading orchestrator |
| `src/index.ts` | Public API → `dist/index.js` bundle entry |
| `package.json` | `files` must include `dist/`; `build` must build `dist/index.js` |

<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->
531 changes: 530 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

97 changes: 94 additions & 3 deletions __tests__/actions/update-scheduled-audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,18 @@
* server actions the dashboard calls (not a reimplementation), so CLI/dashboard
* parity is real: both write through the same `updateConfig`.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";

// `setAutoAuditAction(true)` now refuses without a session — scheduling and
// mailing are one decision, so a timer with nobody to tell is a switch that
// reads as on and produces nothing. These tests are about the CONFIG WRITE, so
// the session check is stubbed to "signed in"; the refusal itself is covered in
// the settings component tests.
const { whoAmIMock, readAuthMock } = vi.hoisted(() => ({
whoAmIMock: vi.fn(),
readAuthMock: vi.fn(),
}));
vi.mock("../../lib/auth/auth-store", () => ({ whoAmI: whoAmIMock, readAuth: readAuthMock }));
import { mkdtempSync, readFileSync, rmSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
Expand All @@ -33,6 +44,10 @@ beforeEach(() => {
home = mkdtempSync(resolve(tmpdir(), "fpai-settings-write-"));
process.env.FAILPROOFAI_HOME = home;
mkdirSync(home, { recursive: true });
whoAmIMock.mockReset().mockResolvedValue({
me: { id: "u1", email: "sidd@exosphere.host", status: "active", created_at: "" },
auth: { user: { id: "u1", email: "sidd@exosphere.host" } },
});
});

afterEach(() => {
Expand All @@ -45,7 +60,7 @@ describe("scheduled-audit write actions", () => {
it("setAutoAuditAction toggles [audit] auto and reflects what the config stored", async () => {
expect(readConfig().audit.auto).toBe(false);
const res = await setAutoAuditAction(true);
expect(res.auto).toBe(true);
expect(res).toEqual({ ok: true, auto: true });
expect(readConfig().audit.auto).toBe(true);
});

Expand Down Expand Up @@ -79,7 +94,10 @@ describe("scheduled-audit write actions", () => {
expect(readConfig().telemetry.enabled).toBe(false);
expect(JSON.parse(readFileSync(configFile(), "utf8")).telemetry).toEqual({ enabled: false });
// And the audit write actually landed alongside it.
expect(readConfig().audit).toEqual({ auto: true, intervalDays: 14 });
expect(readConfig().audit).toMatchObject({ auto: true, intervalDays: 14 });
// Enabling from the dashboard also records consent to send, in the same
// write — that stamp, not `auto`, is what `reportHarm` gates on.
expect(typeof readConfig().audit.reportsConsentedAt).toBe("number");
});

it("preserves an unrelated cloud/collector setting across a scan write", async () => {
Expand All @@ -98,3 +116,76 @@ describe("scheduled-audit write actions", () => {
expect(after.audit.auto).toBe(true);
});
});

describe("a session the server rejects", () => {
it("is REPORTED, not thrown, so the caller can act on it", async () => {
// Next masks a thrown server-action error before the browser sees it — the
// client gets an opaque digest and never the message. A caller matching on
// the text works in development and silently degrades to a generic failure
// in production, which is what shipped: the page showed an address read
// from the local session file, the toggle took the signed-in path, and the
// click dead-ended on "could not turn that on."
whoAmIMock.mockResolvedValue(null);
// `whoAmI()` deletes the session on a 401, so nothing is left on disk.
readAuthMock.mockReturnValue(null);

const res = await setAutoAuditAction(true);

expect(res).toEqual({ ok: false, reason: "signed-out" });
// And nothing was written: a timer with nobody to tell reads as on and
// produces nothing.
expect(readConfig().audit.auto).toBe(false);
});

it("tells an OFFLINE machine apart from an expired one", async () => {
// `whoAmI()` collapses them: null for a 401 and null for every transport
// failure. The client discriminated on `res.ok` alone, so a machine behind
// a proxy or with the wifi down hit the 10s timeout, watched the switch
// snap back, and was told "that sign-in expired" — then handed a code
// prompt that cannot succeed either, which is how a working session gets
// abandoned. What is left ON DISK tells them apart: a 401 wipes it, a
// network failure leaves it.
whoAmIMock.mockResolvedValue(null);
readAuthMock.mockReturnValue({
access_token: "at",
refresh_token: "rt",
access_expires_at: Math.floor(Date.now() / 1000) + 900,
refresh_expires_at: Math.floor(Date.now() / 1000) + 86_400,
user: { id: "u1", email: "sidd@exosphere.host" },
});

const res = await setAutoAuditAction(true);

expect(res).toEqual({ ok: false, reason: "unreachable" });
expect(readConfig().audit.auto).toBe(false);
});

it("calls a session past its refresh window signed-out, not unreachable", async () => {
// The file being present is not the test — a lapsed refresh token cannot
// mint anything, so the code prompt really is the remedy here.
whoAmIMock.mockResolvedValue(null);
readAuthMock.mockReturnValue({
access_token: "at",
refresh_token: "rt",
access_expires_at: Math.floor(Date.now() / 1000) - 7200,
refresh_expires_at: Math.floor(Date.now() / 1000) - 3600,
user: { id: "u1", email: "sidd@exosphere.host" },
});

expect(await setAutoAuditAction(true)).toEqual({ ok: false, reason: "signed-out" });
});

it("still lets somebody turn scheduling OFF", async () => {
// The refusal is one-directional on purpose. An expired session must not
// trap a person into keeping a feature they are trying to disable.
whoAmIMock.mockResolvedValue({ me: { id: "u", email: "a@b.c" } });
await setAutoAuditAction(true);
expect(readConfig().audit.auto).toBe(true);

whoAmIMock.mockResolvedValue(null);
const res = await setAutoAuditAction(false);

expect(res).toEqual({ ok: true, auto: false });
expect(readConfig().audit.auto).toBe(false);
});
});
Loading