Skip to content
Merged
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
189 changes: 189 additions & 0 deletions apps/desktop/src/app/ClerkTokenStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// @effect-diagnostics nodeBuiltinImport:off -- These tests assert real POSIX modes on a temporary directory.
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import { assert, describe, it } from "@effect/vitest";
import { vi } from "vite-plus/test";

vi.mock("node:fs", { spy: true });

import { openClerkTokenStorage, type ClerkTokenStorage } from "./ClerkTokenStore.ts";

// oxlint-disable-next-line t3code/no-global-process-runtime -- Unix mode bits are the assertion; Windows reports them through ACLs.
const describePosix = describe.skipIf(process.platform === "win32");

const modeOf = (path: string): number => NodeFS.statSync(path).mode & 0o777;

function writeLoose(path: string, contents: string): void {
NodeFS.writeFileSync(path, contents, { mode: 0o666 });
NodeFS.chmodSync(path, 0o666);
}

async function withStateDir(run: (stateDir: string) => Promise<void> | void): Promise<void> {
const stateDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-clerk-tokens-"));
try {
await run(stateDir);
} finally {
NodeFS.rmSync(stateDir, { recursive: true, force: true });
}
}

const idleStorage = (): ClerkTokenStorage => ({
getItem: async () => null,
setItem: async () => undefined,
removeItem: () => undefined,
});

describePosix("ClerkTokenStore", () => {
it("keeps clerk tokens at 0600 inside a 0700 directory", async () => {
await withStateDir(async (stateDir) => {
NodeFS.chmodSync(stateDir, 0o755);
const directory = NodePath.join(stateDir, "clerk");
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
const tokenPath = NodePath.join(directory, "clerk-tokens.json");
NodeFS.mkdirSync(directory, { mode: 0o755 });
NodeFS.chmodSync(directory, 0o755);
writeLoose(legacyPath, '{"__clerk_client_jwt":"enc:existing"}\n');

const opened: string[] = [];
const storage = openClerkTokenStorage(stateDir, (cwd) => {
opened.push(cwd);
return {
getItem: async () => {
writeLoose(tokenPath, "from-get");
return "from-get";
},
setItem: (_key, value) => {
writeLoose(tokenPath, value);
},
removeItem: () => {
NodeFS.rmSync(tokenPath, { force: true });
},
};
});

assert.deepEqual(opened, [directory]);
assert.equal(modeOf(stateDir), 0o755);
assert.equal(modeOf(directory), 0o700);
assert.equal(modeOf(tokenPath), 0o600);
assert.equal(NodeFS.existsSync(legacyPath), false);
assert.equal(
NodeFS.readFileSync(tokenPath, "utf8"),
'{"__clerk_client_jwt":"enc:existing"}\n',
);

assert.equal(await storage.getItem("client"), "from-get");
assert.equal(modeOf(tokenPath), 0o600);
await storage.setItem("client", "from-set");
assert.equal(modeOf(tokenPath), 0o600);
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "from-set");
storage.removeItem("client");
assert.equal(NodeFS.existsSync(tokenPath), false);

writeLoose(tokenPath, "kept");
writeLoose(legacyPath, "stale");
NodeFS.chmodSync(directory, 0o755);
openClerkTokenStorage(stateDir, () => idleStorage());
assert.equal(modeOf(directory), 0o700);
assert.equal(modeOf(tokenPath), 0o600);
assert.equal(NodeFS.existsSync(legacyPath), false);
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "kept");
});
});

it("removes the legacy token file when the private copy already exists", async () => {
await withStateDir((stateDir) => {
const directory = NodePath.join(stateDir, "clerk");
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
const tokenPath = NodePath.join(directory, "clerk-tokens.json");
NodeFS.mkdirSync(directory, { mode: 0o755 });
writeLoose(legacyPath, "stale-credential");
writeLoose(tokenPath, "kept-credential");

openClerkTokenStorage(stateDir, () => idleStorage());

assert.equal(NodeFS.existsSync(legacyPath), false);
assert.equal(modeOf(directory), 0o700);
assert.equal(modeOf(tokenPath), 0o600);
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "kept-credential");
});
});

it("resolves a relative state directory before opening storage", async () => {
await withStateDir(async (stateDir) => {
const relativeStateDir = NodePath.relative(process.cwd(), stateDir);
const opened: string[] = [];
openClerkTokenStorage(relativeStateDir, (directory) => {
opened.push(directory);
return idleStorage();
});

const directory = NodePath.join(stateDir, "clerk");
assert.equal(NodePath.isAbsolute(relativeStateDir), false);
assert.deepEqual(opened, [directory]);
assert.equal(modeOf(directory), 0o700);
});
});

it("migrates relative state paths from Electron userData, not the working directory", async () => {
await withStateDir((stateDir) => {
const relativeStateDir = NodePath.relative(process.cwd(), stateDir);
const userData = NodePath.join(
stateDir,
"electron",
...relativeStateDir.split(NodePath.sep).map(() => "profile"),
);
const legacyStateDir = NodePath.join(userData, relativeStateDir);
const legacyPath = NodePath.join(legacyStateDir, "clerk-tokens.json");
NodeFS.mkdirSync(legacyStateDir, { recursive: true });
writeLoose(legacyPath, "existing-session");
const unrelatedPath = NodePath.join(stateDir, "clerk-tokens.json");
writeLoose(unrelatedPath, "not-the-electron-session");

openClerkTokenStorage(relativeStateDir, idleStorage, userData);

const tokenPath = NodePath.join(stateDir, "clerk", "clerk-tokens.json");
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "existing-session");
assert.equal(modeOf(tokenPath), 0o600);
assert.equal(NodeFS.existsSync(legacyPath), false);
assert.equal(NodeFS.readFileSync(unrelatedPath, "utf8"), "not-the-electron-session");
});
});

it("migrates tokens when the legacy directory is on another filesystem", async () => {
await withStateDir((stateDir) => {
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
writeLoose(legacyPath, "existing-session");
using _rename = vi.spyOn(NodeFS, "renameSync").mockImplementation(() => {
throw Object.assign(new Error("cross-device link"), { code: "EXDEV" });
});

openClerkTokenStorage(stateDir, idleStorage);

const tokenPath = NodePath.join(stateDir, "clerk", "clerk-tokens.json");
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "existing-session");
assert.equal(modeOf(tokenPath), 0o600);
assert.equal(NodeFS.existsSync(legacyPath), false);
});
});

it("does not follow a symlinked clerk directory or token file", async () => {
await withStateDir(async (stateDir) => {
const outsideDirectory = NodePath.join(stateDir, "outside-dir");
NodeFS.mkdirSync(outsideDirectory, { mode: 0o755 });
NodeFS.chmodSync(outsideDirectory, 0o755);
NodeFS.symlinkSync(outsideDirectory, NodePath.join(stateDir, "clerk"));

assert.throws(() => openClerkTokenStorage(stateDir, idleStorage), /must be a real directory/);
assert.equal(modeOf(outsideDirectory), 0o755);
NodeFS.unlinkSync(NodePath.join(stateDir, "clerk"));

const outsideFile = NodePath.join(stateDir, "outside.json");
NodeFS.writeFileSync(outsideFile, "secret", { mode: 0o644 });
NodeFS.chmodSync(outsideFile, 0o644);
NodeFS.symlinkSync(outsideFile, NodePath.join(stateDir, "clerk-tokens.json"));
assert.throws(() => openClerkTokenStorage(stateDir, idleStorage), /must be a regular file/);
assert.equal(modeOf(outsideFile), 0o644);
});
});
});
117 changes: 117 additions & 0 deletions apps/desktop/src/app/ClerkTokenStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// @effect-diagnostics nodeBuiltinImport:off -- The Clerk bridge must lock the token file down before startup can yield to Electron's ready event.
import * as NodeFS from "node:fs";
import * as NodePath from "node:path";

// electron-store chmods every write to 0666; the private directory protects tokens during writes.
const CLERK_TOKEN_DIRECTORY_MODE = 0o700;
const CLERK_TOKEN_FILE_MODE = 0o600;
const CLERK_TOKEN_DIRECTORY_NAME = "clerk";
const CLERK_TOKEN_FILE_NAME = "clerk-tokens.json";

type Awaitable<T> = T | Promise<T>;

export interface ClerkTokenStorage {
readonly getItem: (key: string) => Awaitable<string | null>;
readonly setItem: (key: string, value: string) => Awaitable<void>;
readonly removeItem: (key: string) => Awaitable<void>;
}

function isErrno(error: unknown, code: string): boolean {
return typeof error === "object" && error !== null && "code" in error && error.code === code;
}

function lstatIfExists(path: string): NodeFS.Stats | undefined {
try {
return NodeFS.lstatSync(path);
} catch (error) {
if (isErrno(error, "ENOENT")) return undefined;
throw error;
}
}

function assertRegularFile(path: string, stat: NodeFS.Stats): void {
if (stat.isSymbolicLink() || !stat.isFile()) {
throw new Error(`Clerk token file "${path}" must be a regular file.`);
}
}

function restrictClerkTokenFile(path: string): void {
const stat = lstatIfExists(path);
if (stat === undefined) return;
assertRegularFile(path, stat);
if ((stat.mode & 0o777) === CLERK_TOKEN_FILE_MODE) return;
NodeFS.chmodSync(path, CLERK_TOKEN_FILE_MODE);
}

function migrateLegacyClerkTokenFile(stateDir: string, directory: string): void {
const legacyPath = NodePath.join(stateDir, CLERK_TOKEN_FILE_NAME);
const tokenPath = NodePath.join(directory, CLERK_TOKEN_FILE_NAME);
const legacy = lstatIfExists(legacyPath);
if (legacy !== undefined) {
assertRegularFile(legacyPath, legacy);
NodeFS.chmodSync(legacyPath, CLERK_TOKEN_FILE_MODE);
}

const destination = lstatIfExists(tokenPath);
if (destination === undefined) {
if (legacy === undefined) return;
try {
NodeFS.copyFileSync(legacyPath, tokenPath, NodeFS.constants.COPYFILE_EXCL);
NodeFS.chmodSync(tokenPath, CLERK_TOKEN_FILE_MODE);
NodeFS.unlinkSync(legacyPath);
} catch (error) {
if (!isErrno(error, "ENOENT")) throw error;
}
return;
}

assertRegularFile(tokenPath, destination);
NodeFS.chmodSync(tokenPath, CLERK_TOKEN_FILE_MODE);
if (legacy === undefined) return;
try {
NodeFS.unlinkSync(legacyPath);
} catch (error) {
if (!isErrno(error, "ENOENT")) throw error;
}
}

function restrictAfter<T>(run: () => Awaitable<T>, tighten: () => void): Awaitable<T> {
let result: Awaitable<T>;
try {
result = run();
} catch (error) {
tighten();
throw error;
}
if (result instanceof Promise) return result.finally(tighten);
tighten();
return result;
}

/** Opens Clerk token storage in a private directory and keeps the file at `0600`. */
export function openClerkTokenStorage(
stateDir: string,
createStorage: (directory: string) => ClerkTokenStorage,
userDataPath = ".",
): ClerkTokenStorage {
const absoluteStateDir = NodePath.resolve(stateDir);
const directory = NodePath.join(absoluteStateDir, CLERK_TOKEN_DIRECTORY_NAME);
NodeFS.mkdirSync(directory, { recursive: true, mode: CLERK_TOKEN_DIRECTORY_MODE });
const stat = NodeFS.lstatSync(directory);
if (stat.isSymbolicLink() || !stat.isDirectory()) {
throw new Error(`Clerk token directory "${directory}" must be a real directory.`);
}
NodeFS.chmodSync(directory, CLERK_TOKEN_DIRECTORY_MODE);
// electron-store rebases relative paths under Electron userData, not the working directory.
migrateLegacyClerkTokenFile(NodePath.resolve(userDataPath, stateDir), directory);
const tokenPath = NodePath.join(directory, CLERK_TOKEN_FILE_NAME);
restrictClerkTokenFile(tokenPath);

const storage = createStorage(directory);
const tighten = () => restrictClerkTokenFile(tokenPath);
return {
getItem: (key) => restrictAfter(() => storage.getItem(key), tighten),
setItem: (key, value) => restrictAfter(() => storage.setItem(key, value), tighten),
removeItem: (key) => restrictAfter(() => storage.removeItem(key), tighten),
};
}
20 changes: 16 additions & 4 deletions apps/desktop/src/app/DesktopClerk.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
// @effect-diagnostics nodeBuiltinImport:off globalFetchInEffect:off - Hosted handoff test uses a real localhost listener without an OpenAI account.
import * as NodeFS from "node:fs";
import * as NodeHttp from "node:http";
import * as NodePath from "@effect/platform-node/NodePath";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import * as NodePathLayer from "@effect/platform-node/NodePath";
import { codexAuthHandoffUrl, readCodexAuthDelivery } from "@t3tools/shared/codexAuthHandoff";
import { EnvironmentId, ProviderInstanceId } from "@t3tools/contracts";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { beforeEach, vi } from "vite-plus/test";
import { afterAll, beforeEach, vi } from "vite-plus/test";

const clerkStateDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-desktop-clerk-"));
afterAll(() => {
NodeFS.rmSync(clerkStateDir, { recursive: true, force: true });
});

const { createClerkBridgeMock, requestSingleInstanceLockMock, storageAdapter, storageMock } =
vi.hoisted(() => ({
Expand Down Expand Up @@ -60,7 +68,7 @@ const layerDesktopClerk = (
isPackaged = false,
) => {
const environment = DesktopEnvironment.DesktopEnvironment.of({
stateDir: "/tmp/t3-state",
stateDir: clerkStateDir,
isDevelopment,
appDataDirectory: "/tmp/app-data",
platform,
Expand Down Expand Up @@ -96,7 +104,7 @@ const layerDesktopClerk = (
return DesktopClerk.layer.pipe(
Layer.provide(
Layer.mergeAll(
NodePath.layerPosix,
NodePathLayer.layerPosix,
Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment),
Layer.succeed(ElectronApp.ElectronApp, electronApp),
Layer.succeed(ElectronShell.ElectronShell, shell),
Expand Down Expand Up @@ -199,6 +207,10 @@ describe("DesktopClerk", () => {
"on:open-url",
"createClerkBridge",
]);
assert.deepEqual(storageMock.mock.calls, [
[{ path: NodePath.join(NodePath.resolve(clerkStateDir), "clerk") }],
]);
assert.notStrictEqual(createClerkBridgeMock.mock.calls[0]?.[0]?.storage, storageAdapter);
},
);

Expand Down
15 changes: 13 additions & 2 deletions apps/desktop/src/app/DesktopClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as ElectronProtocol from "../electron/ElectronProtocol.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
import * as DesktopUserData from "./DesktopUserData.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import { openClerkTokenStorage } from "./ClerkTokenStore.ts";
import * as DesktopWebLinks from "./DesktopWebLinks.ts";

declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined;
Expand Down Expand Up @@ -85,9 +86,18 @@ export const desktopClerkFrontendApiHostname = resolveDesktopClerkFrontendApiHos
: __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__,
);

function createDesktopClerkBridge(stateDir: string, isDevelopment: boolean, version: string) {
function createDesktopClerkBridge(
stateDir: string,
isDevelopment: boolean,
version: string,
userDataPath: string,
) {
return createClerkBridge({
storage: storage({ path: stateDir }),
storage: openClerkTokenStorage(
stateDir,
(directory) => storage({ path: directory }),
userDataPath,
),
manageSingleInstanceLock: false,
passkeys: true,
renderer: {
Expand Down Expand Up @@ -137,6 +147,7 @@ export const make = Effect.gen(function* () {
environment.stateDir,
environment.isDevelopment,
environment.appVersion,
userDataPath,
),
catch: (cause) =>
new DesktopClerkBridgeInitializationError({
Expand Down
Loading