diff --git a/apps/desktop/src/app/ClerkTokenStorage.test.ts b/apps/desktop/src/app/ClerkTokenStorage.test.ts new file mode 100644 index 000000000000..da2f7fa6fcbc --- /dev/null +++ b/apps/desktop/src/app/ClerkTokenStorage.test.ts @@ -0,0 +1,54 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, describe, it } from "@effect/vitest"; +import * as HostProcess from "@t3tools/shared/HostProcess"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import { vi } from "vite-plus/test"; + +vi.mock("electron", () => ({ + safeStorage: { + isEncryptionAvailable: () => true, + encryptString: (value: string) => Buffer.from(value), + decryptString: (value: Buffer) => value.toString(), + }, +})); + +import { storage } from "@clerk/electron/storage"; + +const fileMode = (path: string) => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const info = yield* fileSystem.stat(path); + return info.mode & 0o777; + }); + +describe("Clerk token storage", () => { + it.effect("writes the token file readable and writable only by its owner", () => + Effect.gen(function* () { + if ((yield* HostProcess.Platform) === "win32") return; + const fileSystem = yield* FileSystem.FileSystem; + const stateDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-clerk-tokens-" }); + + yield* Effect.promise(async () => { + await storage({ path: stateDir }).setItem("__clerk_client_jwt", "t"); + }); + + assert.equal(yield* fileMode(`${stateDir}/clerk-tokens.json`), 0o600); + }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), + ); + + it.effect("tightens a token file left world-writable by an earlier version", () => + Effect.gen(function* () { + if ((yield* HostProcess.Platform) === "win32") return; + const fileSystem = yield* FileSystem.FileSystem; + const stateDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-clerk-tokens-" }); + const tokenFile = `${stateDir}/clerk-tokens.json`; + yield* fileSystem.writeFileString(tokenFile, "{}"); + yield* fileSystem.chmod(tokenFile, 0o666); + + storage({ path: stateDir }); + + assert.equal(yield* fileMode(tokenFile), 0o600); + }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), + ); +}); diff --git a/patches/@clerk__electron@0.0.44.patch b/patches/@clerk__electron@0.0.44.patch new file mode 100644 index 000000000000..7909c8b83915 --- /dev/null +++ b/patches/@clerk__electron@0.0.44.patch @@ -0,0 +1,43 @@ +diff --git a/dist/cjs/storage/index.js b/dist/cjs/storage/index.js +index 7adc3a0df931931393e2336631f3b3f4c60fdbab..f628e546e5514e039b2bf91e87385d28a4e9c9e2 100644 +--- a/dist/cjs/storage/index.js ++++ b/dist/cjs/storage/index.js +@@ -98,8 +98,12 @@ async function resolveCipher() { + function storage(options = {}) { + const store = new electron_store.default({ + name: options.name ?? "clerk-tokens", +- ...options.path ? { cwd: options.path } : {} ++ ...options.path ? { cwd: options.path } : {}, ++ configFileMode: 0o600 + }); ++ try { ++ require("node:fs").chmodSync(store.path, 0o600); ++ } catch {} + const memoryFallback = /* @__PURE__ */ new Map(); + const mutationVersions = /* @__PURE__ */ new Map(); + const beginMutation = (key) => { +diff --git a/dist/esm/storage/index.js b/dist/esm/storage/index.js +index d289bd84ca4dc6a5e2fc4c6b8f9de5bef1d7a175..195996714ecc36d818ecd480fcdeaf8e5a830d4d 100644 +--- a/dist/esm/storage/index.js ++++ b/dist/esm/storage/index.js +@@ -1,5 +1,6 @@ + import { safeStorage } from "electron"; + import Store from "electron-store"; ++import { chmodSync } from "node:fs"; + + //#region src/storage/index.ts + /** +@@ -69,8 +70,12 @@ async function resolveCipher() { + function storage(options = {}) { + const store = new Store({ + name: options.name ?? "clerk-tokens", +- ...options.path ? { cwd: options.path } : {} ++ ...options.path ? { cwd: options.path } : {}, ++ configFileMode: 0o600 + }); ++ try { ++ chmodSync(store.path, 0o600); ++ } catch {} + const memoryFallback = /* @__PURE__ */ new Map(); + const mutationVersions = /* @__PURE__ */ new Map(); + const beginMutation = (key) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd92295b7f24..e5276939e45e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,6 +143,7 @@ overrides: packageExtensionsChecksum: sha256-FYDaKefjFcda1+vgiq8yMpZC4ehE5CyLi/bsCGLgTtY= patchedDependencies: + '@clerk/electron@0.0.44': ecb83f06bcaf6dd0858e6f7efd2a9de74fdcee1700fc0e4bca4a23bc92018808 '@clerk/expo@4.6.8': b8b22ab733de5bea846ad9e88046c10582352fe9ad86f314f368b2a823f0f281 '@effect/vitest@4.0.2': 595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9 '@expo/metro-config@58.0.6': 90fa980e8b6480631fb2fbb04559834e3c612db40c550cd828f11be297d8d760 @@ -201,7 +202,7 @@ importers: dependencies: '@clerk/electron': specifier: 0.0.44 - version: 0.0.44(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.4.5)(react-dom@19.3.0(react@19.3.0))(react@19.2.6) + version: 0.0.44(patch_hash=ecb83f06bcaf6dd0858e6f7efd2a9de74fdcee1700fc0e4bca4a23bc92018808)(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.4.5)(react-dom@19.3.0(react@19.3.0))(react@19.2.6) '@clerk/electron-passkeys': specifier: 0.0.3 version: 0.0.3 @@ -748,7 +749,7 @@ importers: version: 1.5.0(@date-fns/tz@1.5.0)(@types/react@19.3.0)(date-fns@4.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/electron': specifier: 0.0.44 - version: 0.0.44(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.4.5)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 0.0.44(patch_hash=ecb83f06bcaf6dd0858e6f7efd2a9de74fdcee1700fc0e4bca4a23bc92018808)(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.4.5)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/react': specifier: 6.16.1 version: 6.16.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -13415,7 +13416,7 @@ snapshots: '@clerk/electron-passkeys-win32-arm64-msvc': 0.0.3 '@clerk/electron-passkeys-win32-x64-msvc': 0.0.3 - '@clerk/electron@0.0.44(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.4.5)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/electron@0.0.44(patch_hash=ecb83f06bcaf6dd0858e6f7efd2a9de74fdcee1700fc0e4bca4a23bc92018808)(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.4.5)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@clerk/clerk-js': 6.32.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/react': 6.16.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -13428,7 +13429,7 @@ snapshots: electron-store: 8.2.0 react-dom: 19.2.6(react@19.2.6) - '@clerk/electron@0.0.44(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.4.5)(react-dom@19.3.0(react@19.3.0))(react@19.2.6)': + '@clerk/electron@0.0.44(patch_hash=ecb83f06bcaf6dd0858e6f7efd2a9de74fdcee1700fc0e4bca4a23bc92018808)(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.4.5)(react-dom@19.3.0(react@19.3.0))(react@19.2.6)': dependencies: '@clerk/clerk-js': 6.32.1(react-dom@19.3.0(react@19.3.0))(react@19.2.6) '@clerk/react': 6.16.1(react-dom@19.3.0(react@19.3.0))(react@19.2.6) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c207f7b4801b..80db0d6a0fe8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -287,6 +287,8 @@ patchedDependencies: expo-glass-effect@58.0.3: patches/expo-glass-effect@58.0.3.patch expo-blur@58.0.3: patches/expo-blur@58.0.3.patch expo-audio@58.0.4: patches/expo-audio@58.0.4.patch + # Keep Clerk's token file owner-only; electron-store writes 0666 by default. + "@clerk/electron@0.0.44": patches/@clerk__electron@0.0.44.patch "@clerk/expo@4.6.8": patches/@clerk__expo@4.6.8.patch "@effect/vitest@4.0.2": patches/@effect__vitest@4.0.2.patch "@expo/metro-config@58.0.6": patches/@expo__metro-config@58.0.6.patch