Validate session snapshots and wipe keys on logout (closes #10)
check / check (push) Successful in 40s

Client.fromJSON checks every snapshot field and each key's decoded length
and throws an error naming the bad field. toJSON reads the token through a
new ApiClient.getAuthToken and throws when there is none. logout zeroes the
key buffers in place; collectionsSince re-checks for logout after its
request so it never decrypts with zeroed keys. The CLI now reports a
corrupt session file separately from a missing one.

Model: opus-5-5
This commit is contained in:
2026-09-23 00:06:18 +00:00
parent 3871d6228e
commit 5229769b3c
7 changed files with 319 additions and 33 deletions
+16 -20
View File
@@ -2,13 +2,7 @@
import { input, password as passwordPrompt } from "@inquirer/prompts";
import { stdout, stderr } from "node:process";
import {
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { Command } from "commander";
import envPaths from "env-paths";
@@ -22,6 +16,7 @@ import {
thumbnailName,
} from "../src/cli-output.js";
import { freshCollections, freshFiles, freshFile } from "../src/cli-read.js";
import { loadSession } from "../src/cli-session.js";
import { runMetadataBackup } from "../src/metadata-backup.js";
import {
listMissingThumbnails,
@@ -31,15 +26,6 @@ import {
const paths = envPaths("quak", { suffix: "" });
const sessionPath = join(paths.data, "session.json");
const loadSession = (): ClientSnapshot | null => {
if (!existsSync(sessionPath)) return null;
try {
return JSON.parse(readFileSync(sessionPath, "utf-8")) as ClientSnapshot;
} catch {
return null;
}
};
const saveSession = (snapshot: ClientSnapshot): void => {
mkdirSync(paths.data, { recursive: true, mode: 0o700 });
writeFileSync(sessionPath, JSON.stringify(snapshot, null, 2), {
@@ -48,14 +34,23 @@ const saveSession = (snapshot: ClientSnapshot): void => {
};
const requireSession = (): Client => {
const snapshot = loadSession();
if (!snapshot) {
let client: Client | null;
try {
client = loadSession(sessionPath);
} catch (err) {
stderr.write(
`${err instanceof Error ? err.message : err}\n` +
`Run "quak logout" and then "quak login" to replace it.\n`,
);
process.exit(1);
}
if (!client) {
stderr.write(
`Not logged in. Run "quak login" first.\nSession file: ${sessionPath}\n`,
);
process.exit(1);
}
return Client.fromJSON(snapshot);
return client;
};
const prompt = async (message: string): Promise<string> => input({ message });
@@ -157,7 +152,8 @@ program
program
.command("whoami")
.description("Print the logged-in account")
.action(() => {
.action(async () => {
await init();
const client = requireSession();
const info = client.whoami();
stdout.write(JSON.stringify(info) + "\n");