cedarstalking with keyboard shortcuts
1import { environment, LocalStorage } from "@raycast/api";
2import { exec, spawn } from "child_process";
3import {
4 access,
5 mkdir,
6 readFile,
7 symlink,
8 unlink,
9 writeFile,
10} from "fs/promises";
11import * as os from "os";
12import * as path from "path";
13import { promisify } from "util";
14
15const execAsync = promisify(exec);
16const COOKIE_KEY = "session_cookie";
17
18// ─── Cookie storage ────────────────────────────────────────────────────────
19
20export async function getStoredCookie(): Promise<string | undefined> {
21 return LocalStorage.getItem<string>(COOKIE_KEY);
22}
23
24export async function storeCookie(cookie: string): Promise<void> {
25 await LocalStorage.setItem(COOKIE_KEY, cookie);
26}
27
28export async function clearCookie(): Promise<void> {
29 await LocalStorage.removeItem(COOKIE_KEY);
30}
31
32// ─── Auth browser ──────────────────────────────────────────────────────────
33
34// Opens an isolated WKWebView window via a temporary .app bundle so macOS
35// grants it proper window-server access. Cookie is returned through a temp file.
36export async function launchAuthBrowser(signInUrl: string): Promise<string> {
37 const binaryPath = await ensureBinary();
38 const appBundle = await ensureAppBundle(binaryPath);
39 const cookieFile = path.join(os.tmpdir(), "cedarstalk-cookie.txt");
40
41 await unlink(cookieFile).catch(() => {});
42
43 // Use spawn (not execAsync) so the SAML URL isn't shell-interpreted
44 await new Promise<void>((resolve, reject) => {
45 const proc = spawn(
46 "open",
47 ["-n", "-W", appBundle, "--args", signInUrl, cookieFile],
48 {
49 stdio: "ignore",
50 },
51 );
52 proc.on("close", (code) => {
53 // open -W exits 0 whether the app succeeded or cancelled;
54 // we detect success by whether the cookie file was written
55 resolve();
56 });
57 proc.on("error", reject);
58 });
59
60 const cookie = await readFile(cookieFile, "utf-8")
61 .then((s) => s.trim())
62 .catch(() => "");
63 await unlink(cookieFile).catch(() => {});
64
65 if (!cookie) throw new Error("Sign-in cancelled");
66 return cookie;
67}
68
69async function ensureBinary(): Promise<string> {
70 const swiftSrc = path.join(environment.assetsPath, "auth-browser.swift");
71 const binaryPath = path.join(environment.supportPath, "auth-browser");
72
73 try {
74 await access(binaryPath);
75 return binaryPath;
76 } catch {
77 await mkdir(environment.supportPath, { recursive: true });
78 // Compile with optimisations to avoid debug-mode assertion traps
79 await execAsync(`swiftc -O "${swiftSrc}" -o "${binaryPath}"`);
80 // Ad-hoc sign so macOS treats it as a trusted binary
81 await execAsync(`codesign --sign - --force "${binaryPath}"`);
82 return binaryPath;
83 }
84}
85
86async function ensureAppBundle(binaryPath: string): Promise<string> {
87 const appDir = path.join(os.tmpdir(), "CedarStalkAuth.app");
88 const macosDir = path.join(appDir, "Contents", "MacOS");
89 const plistPath = path.join(appDir, "Contents", "Info.plist");
90 const bundledBinary = path.join(macosDir, "CedarStalkAuth");
91
92 await mkdir(macosDir, { recursive: true });
93
94 // Always recreate the symlink so it tracks the current binary path
95 await unlink(bundledBinary).catch(() => {});
96 await symlink(binaryPath, bundledBinary);
97
98 await writeFile(
99 plistPath,
100 `<?xml version="1.0" encoding="UTF-8"?>
101<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
102<plist version="1.0">
103<dict>
104 <key>CFBundlePackageType</key><string>APPL</string>
105 <key>CFBundleExecutable</key><string>CedarStalkAuth</string>
106 <key>CFBundleIdentifier</key><string>sh.dunkirk.cedarstalk.auth</string>
107 <key>CFBundleName</key><string>CedarStalk Auth</string>
108 <key>NSPrincipalClass</key><string>NSApplication</string>
109 <key>NSHighResolutionCapable</key><true/>
110 <key>LSMinimumSystemVersion</key><string>13.0</string>
111</dict>
112</plist>`,
113 );
114
115 return appDir;
116}