Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1361,7 +1361,7 @@ export class BetterWright {
synced: 0,
selected: extracted.selected,
skipped: extracted.skipped,
source: extracted.source,
source: "path" in extracted.source ? { path: "selected" } : extracted.source,
target: consentTarget || (this.hostTarget ? "host" : "local"),
warnings: extracted.warnings,
};
Expand Down
3 changes: 3 additions & 0 deletions src/cookie-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ export function normalizeCookieSyncOptions(
if (includeSessionValue !== undefined && !isBoolean(includeSessionValue)) {
throw new TypeError("syncCookies includeSession must be a boolean.");
}
if (includeSessionValue === true && "path" in source) {
throw new TypeError("includeSession is unavailable for explicit cookie paths.");
}
const appBoundValue = untrustedField(value, "windowsAppBound");
const windowsAppBound = appBoundValue === undefined
? "disabled"
Expand Down
16 changes: 14 additions & 2 deletions src/daemon-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import { spawn } from "node:child_process";
import fs from "node:fs";
import net from "node:net";
import path from "node:path";
import readline from "node:readline";

import {
Expand All @@ -27,7 +28,7 @@ import {
} from "./daemon.js";
import { profileLabel, resolveProfileName } from "./profile-name.js";
import { bunInheritedExecArgv } from "./runtime.js";
import type { UntrustedValue } from "./untrusted-value.js";
import { isRecord, isString, type UntrustedValue, untrustedField } from "./untrusted-value.js";

const CONNECT_TIMEOUT_MS = 1_000;
const SPAWN_WAIT_MS = 8_000;
Expand Down Expand Up @@ -250,6 +251,17 @@ async function waitForSocketGone(socketPath, timeoutMs = 5_000) {
* | {ok: false, reason: string}
* >}
*/
// The daemon keeps the cwd of whichever CLI spawned it, so a relative
// cookie path must be resolved here, against the caller's directory.
function callerRelativeCookieSource(options: UntrustedValue) {
const source = isRecord(options) ? untrustedField(options, "source") : undefined;
const sourcePath = isRecord(source) ? untrustedField(source, "path") : undefined;
if (!isRecord(options) || !isRecord(source) || !isString(sourcePath) || !sourcePath.trim() || path.isAbsolute(sourcePath.trim())) {
return options;
}
return { ...options, source: { ...source, path: path.resolve(sourcePath.trim()) } };
}

export async function connectSessionDaemon({
home = defaultDaemonHome(),
cliPath,
Expand Down Expand Up @@ -376,7 +388,7 @@ export function createDaemonBrowser(channel, { session = "default" }: any = {})
syncCookies: (options?: any) =>
call(
"syncCookies",
[options],
[callerRelativeCookieSource(options)],
Math.min(
Math.max(Number(options?.timeoutMs) || 30_000, 1_000) + 45_000,
180_000,
Expand Down
8 changes: 7 additions & 1 deletion src/launch-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,13 @@ export function launchOptionsFromEnv(home = defaultHome(), env = process.env): L
}

export function resolveLaunchOptions(options: BetterWrightOptions): BetterWrightOptions {
const defaults = launchOptionsFromEnv(options.home || defaultHome());
// Lower-priority variables cannot reject a setting the caller supplied.
const env = { ...process.env };
if (options.headless !== undefined) delete env.BETTERWRIGHT_HEADLESS;
if (options.adBlock !== undefined) delete env.BETTERWRIGHT_AD_BLOCK;
if (options.profile !== undefined) delete env.BETTERWRIGHT_PROFILE;
if (options.downloadPolicy !== undefined) delete env.BETTERWRIGHT_DOWNLOAD_POLICY;
const defaults = launchOptionsFromEnv(options.home || defaultHome(), env);
// A forced display mode must also cancel an inherited off-screen window.
if (options.headless !== undefined && options.headedInvisible === undefined) defaults.headedInvisible = false;
return { ...options, ...defaults, ...Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined)) };
Expand Down
18 changes: 18 additions & 0 deletions tests/node/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,24 @@ test("the worker rechecks Cookie Sync consent before opening a remote CDP target
}
});

test("an empty Cookie Sync result does not echo an explicit source path", async () => {
const hostTarget = {
connect: async () => ({ provider: { cdpUrl: "ws://127.0.0.1:9/devtools/browser/unreachable" }, close: async () => {} }),
};
const browser = new BetterWright({ hostTarget, vault: false });
browser._extractCookieSync = async (normalized) => ({
cookies: [], selected: 0, skipped: 1, warnings: [], source: { ...normalized.source },
});
try {
const empty = await browser.syncCookies({ source: { path: "/private/fixture/Cookies" }, domains: ["example.test"] });
assert.equal(empty.ok, true);
assert.deepEqual(empty.source, { path: "selected" });
assert.equal(JSON.stringify(empty).includes("/private/fixture"), false);
} finally {
await browser.close();
}
});

test("Cookie Sync to a host-owned target needs no cloud consent and reports the granted domains", async () => {
const hostTarget = {
connect: async () => ({ provider: { cdpUrl: "ws://127.0.0.1:9/devtools/browser/unreachable" }, close: async () => {} }),
Expand Down
5 changes: 4 additions & 1 deletion tests/node/cookie-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ test("explicit Chromium sources read plaintext cookies outside the registry with
}
assert.throws(() => normalizeCookieSyncOptions({ source: { browser: "chrome", path: profile } }), /cannot be combined/);
assert.throws(() => normalizeCookieSyncOptions({ source: { path: "\n" } }), /single-line/);
await assert.rejects(extractCookieSync(normalizeCookieSyncOptions({ source: { path: profile }, includeSession: true })), /includeSession/);
assert.throws(
() => normalizeCookieSyncOptions({ source: { path: profile }, includeSession: true }),
/includeSession is unavailable for explicit cookie paths/,
);
});

test("Cookie Sync normalizes source, domains, timeout, and secure defaults", () => {
Expand Down
7 changes: 7 additions & 0 deletions tests/node/daemon.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import net from "node:net";
import path from "node:path";
import readline from "node:readline";
import test from "node:test";
import {
Expand Down Expand Up @@ -355,6 +356,12 @@ test("Cookie Sync crosses the daemon socket as source options, not cookie plaint
assert.equal(result.synced, 4);
assert.deepEqual(browser.calls.at(-1), ["syncCookies", options]);
assert.equal(JSON.stringify(browser.calls.at(-1)).includes('"cookies"'), false);
const relative = { source: { path: path.join("profiles", "Default") }, domains: ["example.com"] };
assert.equal((await proxy.syncCookies(relative)).ok, true);
assert.deepEqual(browser.calls.at(-1), ["syncCookies", {
...relative,
source: { path: path.resolve("profiles", "Default") },
}]);
await proxy.close();
} finally {
await cleanup();
Expand Down
23 changes: 23 additions & 0 deletions tests/node/launch-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,29 @@ test("environment and explicit constructor options override saved launch default
}
});

test("invalid environment values do not reject explicitly supplied constructor options", async () => {
const home = makeTempDir("betterwright-launch-explicit-");
const invalid = { BETTERWRIGHT_HEADLESS: "maybe", BETTERWRIGHT_AD_BLOCK: "invalid",
BETTERWRIGHT_PROFILE: "../escape", BETTERWRIGHT_DOWNLOAD_POLICY: "sometimes" };
const previous = Object.fromEntries(Object.keys(invalid).map(key => [key, process.env[key]]));
Object.assign(process.env, invalid);
try {
const browser = new BetterWright({ home, vault: false, headless: true, adBlock: false,
profile: "explicit", downloadPolicy: "ask" });
assert.equal(browser.headless, true);
assert.equal(browser.adBlock, false);
assert.equal(browser.profile, "explicit");
assert.equal(browser.downloadPolicy, "ask");
await browser.close();
assert.throws(() => new BetterWright({ home, vault: false, headless: true }), /BETTERWRIGHT_AD_BLOCK/);
} finally {
for (const [key, value] of Object.entries(previous)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
});

test("CLI daemon config includes saved identity, display and download policy in its signature", () => {
const home = makeTempDir("betterwright-launch-daemon-");
const file = path.join(home, "config.json");
Expand Down
Loading