From ddaa8a2bb7053a774928bd355b07e4d4839cb05b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 21 Apr 2026 14:25:03 -0600 Subject: [PATCH] fix(build): install all platform keyring bindings for cross-compile Cross-compile runs on a Linux host, which by default skips @napi-rs/keyring optional deps for darwin and win32. Without those bindings present at bundle time, Bun rewrites the require calls to throw stubs, so every non-Linux binary silently fell back to plaintext-file credential storage at runtime. Pass --cpu='*' --os='*' to bun install in the build workflow so every target's native binding is resolvable when the bundler runs, and add a preflight check in scripts/build.ts that fails fast with the fix command if the bindings are still missing. --- .changeset/fix-keyring-cross-compile.md | 8 +++++++ .github/workflows/build-binaries.yml | 9 +++++++- scripts/build.ts | 30 ++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-keyring-cross-compile.md diff --git a/.changeset/fix-keyring-cross-compile.md b/.changeset/fix-keyring-cross-compile.md new file mode 100644 index 00000000..9b8e1c34 --- /dev/null +++ b/.changeset/fix-keyring-cross-compile.md @@ -0,0 +1,8 @@ +--- +"clerk": patch +--- + +Store macOS credentials in the system Keychain instead of a plaintext file. + +- Previously, macOS builds silently stored the OAuth token in `~/Library/Application Support/clerk-cli/credentials` because cross-compiled binaries were missing the native Keychain binding. +- Run `clerk login` after upgrading so the CLI writes a fresh token into the Keychain and removes the old plaintext file. diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 603a7794..3cec1ccd 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -31,7 +31,14 @@ jobs: - uses: oven-sh/setup-bun@v2 with: bun-version: "1.3.11" - - run: bun install --frozen-lockfile + # --cpu=* --os=* forces bun to install every platform's optional deps, + # not just the Linux host's. Cross-compiling with bun build --compile + # embeds each target's native bindings (e.g. @napi-rs/keyring-) + # into the executable; without this flag the darwin and win32 bindings + # are absent from node_modules and the bundler silently replaces them + # with throw stubs, leaving those binaries unable to reach the OS + # keychain and falling back to plaintext-file credential storage. + - run: bun install --frozen-lockfile --cpu='*' --os='*' - name: Build all targets env: diff --git a/scripts/build.ts b/scripts/build.ts index bb291d7b..c6f75661 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -2,7 +2,14 @@ import { mkdir } from "node:fs/promises"; import { join } from "node:path"; import { parseArgs } from "node:util"; import { DEV_CLI_VERSION } from "../packages/cli-core/src/lib/version.ts"; -import { targets } from "./lib/targets.ts"; +import { type Target, targets } from "./lib/targets.ts"; + +function keyringBindingPath(target: Target): string { + const libcSuffix = target.libc === "musl" ? "-musl" : target.libc === "glibc" ? "-gnu" : ""; + const winSuffix = target.os === "win32" ? "-msvc" : ""; + const bindingName = `${target.os}-${target.cpu}${libcSuffix}${winSuffix}`; + return join("node_modules", "@napi-rs", `keyring-${bindingName}`, `keyring.${bindingName}.node`); +} const { values } = parseArgs({ args: Bun.argv.slice(2), @@ -48,6 +55,27 @@ if (selectedTargets.length === 0) { ); } +// Cross-compile embeds each target's @napi-rs/keyring native binding into +// the output. A default `bun install` on any single host skips the bindings +// for other platforms (optional deps are filtered by os/cpu), so without a +// platform-unfiltered install the darwin/win32/arm64 builds would silently +// ship without the binding and fall back to plaintext-file credentials at +// runtime. Fail fast here so the dev sees the actionable fix. +const bindingChecks = await Promise.all( + selectedTargets.map(async (t) => { + const path = keyringBindingPath(t); + return { target: t, path, exists: await Bun.file(path).exists() }; + }), +); +const missingBindings = bindingChecks.filter((check) => !check.exists); +if (missingBindings.length > 0) { + const list = missingBindings.map(({ target, path }) => ` - ${target.name}: ${path}`).join("\n"); + throw new Error( + `Missing @napi-rs/keyring native bindings for:\n${list}\n\n` + + `Run \`bun install --frozen-lockfile --cpu='*' --os='*'\` to install every platform's optional deps.`, + ); +} + console.log(`Building ${selectedTargets.length} target(s) with version ${version}\n`); let failed = false;