- brew-browser stores your token in the macOS Keychain. The token is
+ brew-browser stores your token in the {keyringName}. The token is
never sent over IPC to the renderer,
never written to disk, and never
logged. Only the derived {`{ signedIn, username, scopes }`}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 6c4222c..729d151 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -5,6 +5,8 @@
* `invoke()` returns for each Tauri command.
*/
+import { keyringNameCapitalized } from "$lib/util/platform";
+
// =========================================================
// 2.1 Common enums
// =========================================================
@@ -879,7 +881,7 @@ export function brewErrorMessage(e: BrewErrorPayload): string {
return `GitHub API rate limit reached. Resets at ${reset}. Sign in to lift the limit.`;
}
case "keychain_unavailable":
- return `macOS Keychain unavailable: ${e.message}`;
+ return `${keyringNameCapitalized} unavailable: ${e.message}`;
case "auth_required":
return "Sign in to GitHub to use this feature.";
case "scope_required":
diff --git a/src/lib/util/platform.ts b/src/lib/util/platform.ts
new file mode 100644
index 0000000..c6ec95e
--- /dev/null
+++ b/src/lib/util/platform.ts
@@ -0,0 +1,31 @@
+/**
+ * Host-OS detection for the renderer — the single source of truth for
+ * platform-aware user-facing copy (e.g. "Reveal in Finder" on macOS vs
+ * "Show in file manager" on Linux).
+ *
+ * Why navigator-based and not `@tauri-apps/plugin-os`: that plugin is not a
+ * dependency, and adding one for a handful of label swaps isn't worth the
+ * weight. The WebView's `navigator.userAgent` faithfully reflects the host OS
+ * — macOS reports "Macintosh"/"Mac OS X", Linux reports "Linux"/"X11". That's
+ * more than precise enough for choosing a noun in a button label.
+ *
+ * Evaluated once at module load: the host OS does not change mid-session.
+ */
+const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
+
+/** True when running on macOS. Defaults to true under SSR/no-navigator so the
+ * static build (and any non-WebView render) keeps the macOS wording. */
+export const isMac = ua === "" || /Mac|Macintosh|Mac OS X/i.test(ua);
+
+/** True when running on Linux (X11/Wayland WebView). */
+export const isLinux = /Linux|X11/i.test(ua) && !isMac;
+
+/** The OS file manager's name, for interpolation into copy. */
+export const fileManagerName = isMac ? "Finder" : "file manager";
+
+/** The OS credential store's name, for mid-sentence interpolation. */
+export const keyringName = isMac ? "macOS Keychain" : "system keyring";
+
+/** Sentence-leading form of {@link keyringName} ("macOS Keychain" already
+ * starts uppercase; "system keyring" becomes "System keyring"). */
+export const keyringNameCapitalized = isMac ? "macOS Keychain" : "System keyring";
diff --git a/tools/release/publish-manifest.sh b/tools/release/publish-manifest.sh
index f3d4094..aafb245 100755
--- a/tools/release/publish-manifest.sh
+++ b/tools/release/publish-manifest.sh
@@ -92,6 +92,16 @@ ARTIFACT_RELEASE_NAME="brew-browser_${VERSION}_aarch64.app.tar.gz"
DIST_DIR="$REPO_ROOT/dist"
MANIFEST_PATH="$DIST_DIR/updater.json"
+# Linux updater artifact. Tauri's Linux updater install path uses the
+# .AppImage (NOT the .deb/.rpm), and signs it with the same
+# TAURI_SIGNING_PRIVATE_KEY (minisign is cross-platform). The bundler
+# stamps the AppImage with the version + arch, so glob for it rather
+# than hard-coding the name. This block is OPTIONAL: when running on a
+# Mac that only built the .app.tar.gz, no AppImage exists and we emit a
+# macOS-only manifest exactly as before.
+LINUX_ARTIFACT_PATH="$(ls -t "$REPO_ROOT"/src-tauri/target/release/bundle/appimage/*.AppImage 2>/dev/null | head -1 || true)"
+LINUX_ARTIFACT_RELEASE_NAME="brew-browser_${VERSION}_amd64.AppImage"
+
# ---------- Preflight ----------
if [[ ! -f "$ARTIFACT_PATH" ]]; then
@@ -136,6 +146,50 @@ SIGNATURE_JSON="${SIGNATURE_RAW%$'\n'}"
SIGNATURE_JSON=$(perl -pe 's/\n/\\n/g' <<< "$SIGNATURE_JSON")
SIGNATURE_JSON="${SIGNATURE_JSON%\\n}"
+# ---------- Linux platform block (conditional) ----------
+
+# Build the linux-x86_64 platform entry only when the AppImage + its
+# .sig are both present. Absent (Mac-only build) → LINUX_BLOCK stays
+# empty and the manifest is macOS-only, byte-for-byte as before.
+LINUX_BLOCK=""
+if [[ -n "$LINUX_ARTIFACT_PATH" && -f "$LINUX_ARTIFACT_PATH" ]]; then
+ LINUX_SIGNATURE_FILE="${LINUX_ARTIFACT_PATH}.sig"
+ if [[ ! -f "$LINUX_SIGNATURE_FILE" ]]; then
+ echo "error: found AppImage but its signature is missing:" >&2
+ echo " $LINUX_SIGNATURE_FILE" >&2
+ echo " Tauri produces this when TAURI_SIGNING_PRIVATE_KEY[_PATH] is set" >&2
+ echo " during the Linux 'npm run tauri build'. Re-run with signing env." >&2
+ exit 2
+ fi
+
+ echo "info: computing SHA-256 of $(basename "$LINUX_ARTIFACT_PATH")..." >&2
+ LINUX_SHA256=$(shasum -a 256 "$LINUX_ARTIFACT_PATH" | awk '{print $1}')
+ echo "info: linux sha256 = $LINUX_SHA256" >&2
+
+ # Same single-line Tauri .sig format + defensive newline-escaping as
+ # the macOS signature above.
+ LINUX_SIGNATURE_RAW=$(cat "$LINUX_SIGNATURE_FILE")
+ LINUX_SIGNATURE_JSON="${LINUX_SIGNATURE_RAW%$'\n'}"
+ LINUX_SIGNATURE_JSON=$(perl -pe 's/\n/\\n/g' <<< "$LINUX_SIGNATURE_JSON")
+ LINUX_SIGNATURE_JSON="${LINUX_SIGNATURE_JSON%\\n}"
+
+ LINUX_URL="https://github.com/msitarzewski/brew-browser/releases/download/v${VERSION}/${LINUX_ARTIFACT_RELEASE_NAME}"
+
+ # Leading comma + newline so it appends cleanly after the
+ # darwin-aarch64 entry inside the "platforms" object.
+ LINUX_BLOCK=$(cat <