Skip to content
Open
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
3 changes: 1 addition & 2 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ Local build, packaging, and macOS permission helpers for the packaged Cued runti
| `install-cued-release.sh` | Install the latest GitHub release from the terminal. |
| `request-macos-access.sh` | Trigger or open the relevant macOS privacy prompts and panes. |
| `fetch-node-runtime-macos.sh` | Fetch the Node runtime bundled into the app. |
| `fetch-playwright-chromium-macos.sh` | Fetch the Chromium payload bundled into the app. |
| `fetch-signal-cli-macos.sh` | Fetch the Signal helper payload bundled into the app. |

## App bundle build
Expand All @@ -28,7 +27,7 @@ This script builds the packaged development app at `native/macos/dist/Cued.app`.
- rebuilds the root TypeScript runtime
- rebuilds the native Swift host
- stages production Node dependencies
- bundles the Node runtime and Chromium payload
- bundles the Node runtime
- bundles helper binaries such as the native macOS helper, Signal helper, and WhatsApp helper
- signs the app bundle for local execution

Expand Down
17 changes: 2 additions & 15 deletions scripts/build-cued-daemon-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,10 @@ RESOURCES_DIR="$CONTENTS_DIR/Resources"
RUNTIME_DIR="$RESOURCES_DIR/cued-runtime"
RUNTIME_NODE_ROOT="$RESOURCES_DIR/runtime/node"
RUNTIME_NODE_BIN_DIR="$RUNTIME_NODE_ROOT/bin"
PLAYWRIGHT_CHROMIUM_ROOT="$RESOURCES_DIR/runtime/chromium"
PLAYWRIGHT_CHROMIUM_PAYLOAD_DIR="$PLAYWRIGHT_CHROMIUM_ROOT/chrome"
HELPERS_DIR="$RESOURCES_DIR/helpers"
HELPER_NAME="cued-native-helper"
SIGNAL_FETCH_SCRIPT="$ROOT_DIR/scripts/fetch-signal-cli-macos.sh"
NODE_RUNTIME_FETCH_SCRIPT="$ROOT_DIR/scripts/fetch-node-runtime-macos.sh"
PLAYWRIGHT_CHROMIUM_FETCH_SCRIPT="$ROOT_DIR/scripts/fetch-playwright-chromium-macos.sh"
JIT_RUNTIME_ENTITLEMENTS="$ROOT_DIR/scripts/packaging/jit-runtime.entitlements.plist"
APP_PERMISSIONS_ENTITLEMENTS="$ROOT_DIR/scripts/packaging/app-permissions.entitlements.plist"
SIGNAL_HELPER_SOURCE_DIR="$ROOT_DIR/native/helpers/signal-cli/.build/cued-signal-cli"
Expand Down Expand Up @@ -127,20 +124,14 @@ runtime_entitlements_for_binary() {
"$HELPERS_DIR/$HELPER_NAME")
printf '%s\n' "$APP_PERMISSIONS_ENTITLEMENTS"
;;
"$RUNTIME_NODE_BIN_DIR/node"|"$HELPERS_DIR"/signal-cli/jre/Contents/Home/bin/java|"$PLAYWRIGHT_CHROMIUM_ROOT"/*)
"$RUNTIME_NODE_BIN_DIR/node"|"$HELPERS_DIR"/signal-cli/jre/Contents/Home/bin/java)
printf '%s\n' "$JIT_RUNTIME_ENTITLEMENTS"
;;
esac
}

runtime_entitlements_for_bundle() {
local target="$1"

case "$target" in
"$PLAYWRIGHT_CHROMIUM_ROOT"/*)
printf '%s\n' "$JIT_RUNTIME_ENTITLEMENTS"
;;
esac
:
}

sign_nested_binaries() {
Expand Down Expand Up @@ -234,7 +225,6 @@ pnpm --dir "$ROOT_DIR" build >/dev/null
swift build --package-path "$SWIFT_PACKAGE_DIR" -c release >/dev/null
SWIFT_RESOURCE_BUNDLES=("$SWIFT_PACKAGE_DIR"/.build/*/release/*.bundle)
NODE_RUNTIME_SOURCE_DIR="$(bash "$NODE_RUNTIME_FETCH_SCRIPT" "$NODE_VERSION" "$NODE_ARCH")"
PLAYWRIGHT_CHROMIUM_SOURCE_DIR="$(bash "$PLAYWRIGHT_CHROMIUM_FETCH_SCRIPT")"
bash "$SIGNAL_FETCH_SCRIPT" >/dev/null
mkdir -p "$(dirname "$SLACK_HELPER_SOURCE")"
(cd "$ROOT_DIR/native/helpers/slack-go" && GOWORK=off go build -o "$SLACK_HELPER_SOURCE" .) >/dev/null
Expand All @@ -249,7 +239,6 @@ mkdir -p \
"$RESOURCES_DIR" \
"$RUNTIME_DIR" \
"$RUNTIME_NODE_BIN_DIR" \
"$PLAYWRIGHT_CHROMIUM_ROOT" \
"$HELPERS_DIR"

ditto --noextattr --norsrc "$SWIFT_BINARY" "$MACOS_DIR/$APP_EXECUTABLE_NAME"
Expand All @@ -263,7 +252,6 @@ done
cp "$NODE_RUNTIME_SOURCE_DIR/bin/node" "$RUNTIME_NODE_BIN_DIR/node"
cp -R "$NODE_RUNTIME_SOURCE_DIR/lib" "$RUNTIME_NODE_ROOT/lib"
chmod +x "$RUNTIME_NODE_BIN_DIR/node"
ditto --noextattr --norsrc "$PLAYWRIGHT_CHROMIUM_SOURCE_DIR" "$PLAYWRIGHT_CHROMIUM_PAYLOAD_DIR"
cp -R "$DEPLOY_STAGING_DIR/." "$RUNTIME_DIR/"
cp -R "$SIGNAL_HELPER_SOURCE_DIR" "$HELPERS_DIR/signal-cli"
cp "$SLACK_HELPER_SOURCE" "$HELPERS_DIR/cued-slack-helper"
Expand Down Expand Up @@ -412,7 +400,6 @@ export CUED_NATIVE_BINARY="\${CUED_NATIVE_BINARY:-\$HELPER_BINARY}"
export CUED_IMESSAGE_NATIVE_BINARY="\${CUED_IMESSAGE_NATIVE_BINARY:-\$HELPER_BINARY}"
export CUED_CONTACTS_NATIVE_BINARY="\${CUED_CONTACTS_NATIVE_BINARY:-\$HELPER_BINARY}"
export CUED_AUTH_NATIVE_BINARY="\${CUED_AUTH_NATIVE_BINARY:-\$APP_EXEC}"
export CUED_CHROMIUM_EXECUTABLE_PATH="\${CUED_CHROMIUM_EXECUTABLE_PATH:-\$SCRIPT_DIR/runtime/chromium/chrome/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing}"
if [[ -f "\$SCRIPT_DIR/oauth/google-oauth-client.json" ]]; then
export CUED_BUNDLED_GOOGLE_OAUTH_CLIENT_FILE="\${CUED_BUNDLED_GOOGLE_OAUTH_CLIENT_FILE:-\$SCRIPT_DIR/oauth/google-oauth-client.json}"
fi
Expand Down
11 changes: 2 additions & 9 deletions scripts/build-cued-release-artifacts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,14 @@ runtime_entitlements_for_binary() {
printf '%s\n' "$APP_PERMISSIONS_ENTITLEMENTS"
;;
"$APP_BUNDLE/Contents/Resources/runtime/node/bin/node"|\
"$APP_BUNDLE/Contents/Resources/helpers/signal-cli/jre/Contents/Home/bin/java"|\
"$APP_BUNDLE/Contents/Resources/runtime/chromium"/*)
"$APP_BUNDLE/Contents/Resources/helpers/signal-cli/jre/Contents/Home/bin/java")
printf '%s\n' "$JIT_RUNTIME_ENTITLEMENTS"
;;
esac
}

runtime_entitlements_for_bundle() {
local target="$1"

case "$target" in
"$APP_BUNDLE/Contents/Resources/runtime/chromium"/*)
printf '%s\n' "$JIT_RUNTIME_ENTITLEMENTS"
;;
esac
:
}

sign_macos_binary() {
Expand Down
38 changes: 0 additions & 38 deletions scripts/fetch-playwright-chromium-macos.sh

This file was deleted.

1 change: 1 addition & 0 deletions scripts/validate-cued-release-artifact.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ for executable_path in \
done

for forbidden_path in \
"$RESOURCES_PATH/runtime/chromium" \
"$RUNTIME_PATH/scripts" \
"$RUNTIME_PATH/src" \
"$RUNTIME_PATH/native" \
Expand Down
64 changes: 64 additions & 0 deletions src/platforms/core/auth/browser-resolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import {
type BrowserCandidate,
formatMissingChromiumAuthBrowserError,
listInstalledChromiumAuthBrowsers,
resolveChromiumAuthBrowser,
} from "./browser-resolver.js";

describe("resolveChromiumAuthBrowser", () => {
const candidates: BrowserCandidate[] = [
{
name: "Missing",
executablePath: "/definitely/not/installed",
appBundleIdentifier: "missing.browser",
},
{
name: "Shell",
executablePath: "/bin/sh",
appBundleIdentifier: "test.shell",
},
];

it("uses an explicit executable path override", () => {
expect(
resolveChromiumAuthBrowser(
{
CUED_CHROMIUM_EXECUTABLE_PATH: "/custom/browser",
CUED_CHROMIUM_APP_BUNDLE_IDENTIFIER: "custom.browser",
},
candidates,
),
).toEqual({
name: "Configured Chromium browser",
executablePath: "/custom/browser",
appBundleIdentifier: "custom.browser",
});
});

it("falls back to the first installed candidate", () => {
expect(resolveChromiumAuthBrowser({}, candidates)).toEqual(candidates[1]);
});

it("lists every installed fallback candidate in order", () => {
const installedCandidates = [
candidates[0]!,
candidates[1]!,
{
name: "Also Shell",
executablePath: "/bin/sh",
appBundleIdentifier: "test.shell-alt",
},
];

expect(listInstalledChromiumAuthBrowsers({}, installedCandidates)).toEqual([
candidates[1],
installedCandidates[2],
]);
});

it("returns null when no candidate exists", () => {
expect(resolveChromiumAuthBrowser({}, [candidates[0]!])).toBeNull();
expect(formatMissingChromiumAuthBrowserError()).toContain("No supported browser");
});
});
75 changes: 75 additions & 0 deletions src/platforms/core/auth/browser-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

export interface BrowserCandidate {
name: string;
executablePath: string;
appBundleIdentifier: string;
}

function appExecutablePath(appRoot: string, appName: string, executableName: string): string {
return join(appRoot, `${appName}.app`, "Contents", "MacOS", executableName);
}

function macOSBrowserCandidatesForAppRoot(appRoot: string): BrowserCandidate[] {
return [
{
name: "Google Chrome",
executablePath: appExecutablePath(appRoot, "Google Chrome", "Google Chrome"),
appBundleIdentifier: "com.google.Chrome",
},
{
name: "Chromium",
executablePath: appExecutablePath(appRoot, "Chromium", "Chromium"),
appBundleIdentifier: "org.chromium.Chromium",
},
{
name: "Microsoft Edge",
executablePath: appExecutablePath(appRoot, "Microsoft Edge", "Microsoft Edge"),
appBundleIdentifier: "com.microsoft.edgemac",
},
{
name: "Brave Browser",
executablePath: appExecutablePath(appRoot, "Brave Browser", "Brave Browser"),
appBundleIdentifier: "com.brave.Browser",
},
];
}

export const MACOS_BROWSER_CANDIDATES: BrowserCandidate[] = [
...macOSBrowserCandidatesForAppRoot("/Applications"),
...macOSBrowserCandidatesForAppRoot(join(homedir(), "Applications")),
];

export function listInstalledChromiumAuthBrowsers(
env: NodeJS.ProcessEnv = process.env,
candidates: BrowserCandidate[] = MACOS_BROWSER_CANDIDATES,
): BrowserCandidate[] {
const configured = env.CUED_CHROMIUM_EXECUTABLE_PATH?.trim();
if (configured) {
return [
{
name: "Configured Chromium browser",
executablePath: configured,
appBundleIdentifier: env.CUED_CHROMIUM_APP_BUNDLE_IDENTIFIER?.trim() || "com.google.Chrome",
},
];
}

return candidates.filter((candidate) => existsSync(candidate.executablePath));
}

export function resolveChromiumAuthBrowser(
env: NodeJS.ProcessEnv = process.env,
candidates: BrowserCandidate[] = MACOS_BROWSER_CANDIDATES,
): BrowserCandidate | null {
return listInstalledChromiumAuthBrowsers(env, candidates)[0] ?? null;
}

export function formatMissingChromiumAuthBrowserError(): string {
return [
"No supported browser was found for Cued browser authentication.",
"Install Google Chrome, Chromium, Microsoft Edge, or Brave Browser and try again.",
].join(" ");
}
48 changes: 35 additions & 13 deletions src/platforms/core/auth/chromium-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { execFileSync } from "node:child_process";
import { mkdirSync } from "node:fs";
import { type BrowserContext, type Cookie, chromium, type Page, type Request } from "playwright";
import { cuedAuthKeychainService } from "../../../core/identity.js";
import {
type BrowserCandidate,
formatMissingChromiumAuthBrowserError,
listInstalledChromiumAuthBrowsers,
} from "./browser-resolver.js";

declare const localStorage: {
getItem(key: string): string | null;
Expand Down Expand Up @@ -74,15 +79,11 @@ function getTimeoutMs(): number {
return Number.isFinite(configured) && configured > 0 ? configured : 15 * 60_000;
}

function getExecutablePath(): string | undefined {
return process.env.CUED_CHROMIUM_EXECUTABLE_PATH || undefined;
}

function bringAuthBrowserToFront(): void {
function bringAuthBrowserToFront(browser: BrowserCandidate): void {
try {
execFileSync(
"osascript",
["-e", 'tell application id "com.google.chrome.for.testing" to activate'],
["-e", `tell application id "${browser.appBundleIdentifier}" to activate`],
{
stdio: ["ignore", "ignore", "ignore"],
},
Expand Down Expand Up @@ -475,21 +476,42 @@ async function run(): Promise<void> {
}

mkdirSync(args.profileDir, { recursive: true });
const browsers = listInstalledChromiumAuthBrowsers();
if (browsers.length === 0) {
throw new Error(formatMissingChromiumAuthBrowserError());
}

let context: BrowserContext | null = null;
let browser: BrowserCandidate | null = null;
const launchErrors: string[] = [];
try {
context = await chromium.launchPersistentContext(args.profileDir, {
headless: false,
executablePath: getExecutablePath(),
args: ["--disable-blink-features=AutomationControlled"],
viewport: { width: 1280, height: 900 },
});
for (const candidate of browsers) {
try {
context = await chromium.launchPersistentContext(args.profileDir, {
headless: false,
executablePath: candidate.executablePath,
args: ["--disable-blink-features=AutomationControlled"],
viewport: { width: 1280, height: 900 },
});
browser = candidate;
break;
} catch (error) {
launchErrors.push(
`${candidate.name}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}

if (!context || !browser) {
throw new Error(`Could not launch a supported browser. ${launchErrors.join(" ")}`);
}

let page = firstOpenPage(context) ?? (await context.newPage());
if (!page.url() || page.url() === "about:blank") {
await page.goto(args.launchTarget, { waitUntil: "domcontentloaded" });
}
await page.bringToFront().catch(() => {});
bringAuthBrowserToFront();
bringAuthBrowserToFront(browser);

const deadline = Date.now() + getTimeoutMs();
let lastObservedUrls: string[] = [];
Expand Down
Loading