fix(browser): limit retained action screenshots
This commit is contained in:
1 parent
b5a6a1a9ea
commit
9d4af15d4b
4 files changed
+67
-6
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { BrowserWindow, session } from "electron";
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { readdir, rm, stat } from "node:fs/promises";
|
||||
import { join, resolve, sep } from "node:path";
|
||||
import log from "electron-log/main.js";
|
||||
import type { BrowserAction, BrowserActionResult, QueuedAction } from "./browser/browser-types";
|
||||
@@ -17,6 +17,7 @@ export type { BrowserAction, BrowserActionResult } from "./browser/browser-types
|
||||
* legitimate flow (first-time OAuth with 2FA).
|
||||
*/
|
||||
const ACTION_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
const SCREENSHOT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
export class BrowserManager {
|
||||
private screenshotDir: string;
|
||||
@@ -30,6 +31,25 @@ export class BrowserManager {
|
||||
constructor(profileDir: string) {
|
||||
this.screenshotDir = join(profileDir, "browser-screenshots");
|
||||
if (!existsSync(this.screenshotDir)) mkdirSync(this.screenshotDir, { recursive: true });
|
||||
void this.pruneExpiredScreenshots();
|
||||
}
|
||||
|
||||
private async pruneExpiredScreenshots() {
|
||||
const cutoff = Date.now() - SCREENSHOT_RETENTION_MS;
|
||||
try {
|
||||
const entries = await readdir(this.screenshotDir, { withFileTypes: true });
|
||||
await Promise.all(entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".png"))
|
||||
.map(async (entry) => {
|
||||
const screenshotPath = join(this.screenshotDir, entry.name);
|
||||
const fileStat = await stat(screenshotPath);
|
||||
if (fileStat.mtimeMs < cutoff) {
|
||||
await rm(screenshotPath, { force: true });
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
log.warn(`[browser] Failed to prune expired screenshots: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
private getSession(socialAccountId: string): Electron.Session {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { assertAuthenticatedPage, assertAuthenticatedSession } from "./browser-a
|
||||
import { getPlatformSelectors } from "./platform-selectors";
|
||||
import type { PlatformSelectors } from "./platform-selectors";
|
||||
import { browserVerificationFailed } from "./browser-action-errors";
|
||||
import { shouldCaptureSuccessScreenshot } from "./browser-screenshot-policy";
|
||||
import { isAppError } from "../errors/index.js";
|
||||
|
||||
export { getPlatformRules, assertAllowedUrl, attachWindowSecurity } from "./browser-url-helpers";
|
||||
@@ -226,7 +227,9 @@ export async function runAction(
|
||||
}
|
||||
|
||||
assertNotCancelled();
|
||||
await captureScreenshot(win, screenshotPath, action.id, "success");
|
||||
const successScreenshotPath = shouldCaptureSuccessScreenshot(action)
|
||||
? await captureScreenshot(win, screenshotPath, action.id, "success")
|
||||
: null;
|
||||
await closeWindow(win);
|
||||
logActionTelemetry({
|
||||
platform,
|
||||
@@ -241,13 +244,15 @@ export async function runAction(
|
||||
success: true,
|
||||
cancelled: false,
|
||||
summary,
|
||||
screenshotPath: existsSync(screenshotPath) ? screenshotPath : null,
|
||||
screenshotPath: successScreenshotPath,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
const wasCancelled = cancelledActionIds.has(action.id)
|
||||
|| (error instanceof Error && error.message === "BROWSER_ACTION_CANCELLED");
|
||||
await captureScreenshot(win, screenshotPath, action.id, "failure");
|
||||
const failureScreenshotPath = wasCancelled
|
||||
? null
|
||||
: await captureScreenshot(win, screenshotPath, action.id, "failure");
|
||||
await closeWindow(win);
|
||||
|
||||
const msg = wasCancelled ? "Action cancelled." : describeBrowserActionError(error);
|
||||
@@ -268,7 +273,7 @@ export async function runAction(
|
||||
success: false,
|
||||
cancelled: wasCancelled,
|
||||
summary: wasCancelled ? "Action cancelled" : "Action failed",
|
||||
screenshotPath: existsSync(screenshotPath) ? screenshotPath : null,
|
||||
screenshotPath: failureScreenshotPath,
|
||||
error: msg,
|
||||
};
|
||||
} finally {
|
||||
@@ -316,15 +321,17 @@ async function captureScreenshot(
|
||||
screenshotPath: string,
|
||||
actionId: string,
|
||||
phase: "success" | "failure",
|
||||
): Promise<void> {
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const image: NativeImage = await win.webContents.capturePage();
|
||||
if (!image.isEmpty()) {
|
||||
writeFileSync(screenshotPath, image.toPNG());
|
||||
return existsSync(screenshotPath) ? screenshotPath : null;
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn(`[browser] ${phase} screenshot failed for action ${actionId}: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function closeWindow(win: BrowserWindow): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { BrowserAction } from "./browser-types";
|
||||
|
||||
export function shouldCaptureSuccessScreenshot(action: BrowserAction): boolean {
|
||||
return action.actionType === "screenshot"
|
||||
|| action.payload.captureSuccessScreenshot === true
|
||||
|| action.payload.retainSuccessScreenshot === true;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
assertAuthenticatedPage,
|
||||
assertAuthenticatedSession,
|
||||
} from "../../src/main/browser/browser-action-auth";
|
||||
import { shouldCaptureSuccessScreenshot } from "../../src/main/browser/browser-screenshot-policy";
|
||||
import { getPlatformSelectors } from "../../src/main/browser/platform-selectors";
|
||||
|
||||
function fakeSession(cookiesByOrigin: Record<string, unknown[]>): Pick<Session, "cookies"> {
|
||||
@@ -62,4 +63,30 @@ describe("browser action runtime guards", () => {
|
||||
expect(linkedin.composerOpen).toContain("button.share-box-feed-entry__trigger");
|
||||
expect(linkedin.editor).toContain(".ql-editor[contenteditable='true']");
|
||||
});
|
||||
|
||||
it("captures successful screenshots only for explicit screenshot requests", () => {
|
||||
expect(shouldCaptureSuccessScreenshot({
|
||||
id: "post-1",
|
||||
socialAccountId: "social-1",
|
||||
platform: "twitter",
|
||||
actionType: "post",
|
||||
payload: { content: "hello" },
|
||||
})).toBe(false);
|
||||
|
||||
expect(shouldCaptureSuccessScreenshot({
|
||||
id: "screenshot-1",
|
||||
socialAccountId: "social-1",
|
||||
platform: "twitter",
|
||||
actionType: "screenshot",
|
||||
payload: {},
|
||||
})).toBe(true);
|
||||
|
||||
expect(shouldCaptureSuccessScreenshot({
|
||||
id: "post-2",
|
||||
socialAccountId: "social-1",
|
||||
platform: "twitter",
|
||||
actionType: "post",
|
||||
payload: { content: "hello", captureSuccessScreenshot: true },
|
||||
})).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user