diff --git a/.changeset/electron-package-scaffold.md b/.changeset/electron-package-scaffold.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/electron-package-scaffold.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/electron/LICENSE b/packages/electron/LICENSE new file mode 100644 index 00000000000..daceccfbc84 --- /dev/null +++ b/packages/electron/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Clerk, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/electron/README.md b/packages/electron/README.md new file mode 100644 index 00000000000..08bbb31c2bd --- /dev/null +++ b/packages/electron/README.md @@ -0,0 +1,104 @@ +

+ + + + + + +
+

@clerk/electron

+

+ +
+ +[![Clerk documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs?utm_source=github&utm_medium=clerk_electron) +[![Follow on X](https://img.shields.io/twitter/follow/clerk?style=social)](https://x.com/intent/follow?screen_name=clerk) + +[Changelog](https://github.com/clerk/javascript/blob/main/packages/electron/CHANGELOG.md) +· +[Report a Bug](https://github.com/clerk/javascript/issues/new?assignees=&labels=needs-triage&projects=&template=BUG_REPORT.yml) +· +[Request a Feature](https://feedback.clerk.com/roadmap) +· +[Get help](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_electron) + +
+ +## Getting Started + +[Clerk](https://clerk.com/?utm_source=github&utm_medium=clerk_electron) is the easiest way to add authentication and user management to your Electron application. + +> [!WARNING] +> `@clerk/electron` is under active development and is not yet ready for production use. The API is incomplete and subject to change. + +This package exposes entrypoints for Electron's distinct runtime contexts: + +- `@clerk/electron` — for use in the Electron **main** process. +- `@clerk/electron/preload` — for use in Electron **preload** scripts. +- `@clerk/electron/react` — for use in the Electron **renderer** process. +- `@clerk/electron/storage` — default token storage backed by `electron-store`. + +```ts +// main.ts +import { app, BrowserWindow, net, protocol } from 'electron'; +import { setupMain } from '@clerk/electron'; +import { storage } from '@clerk/electron/storage'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +setupMain({ + storage: storage(), + renderer: { + scheme: 'my-app', + host: 'renderer', + }, +}); + +app.whenReady().then(() => { + protocol.handle('my-app', request => { + const url = new URL(request.url); + const file = url.pathname === '/' ? 'index.html' : url.pathname; + + return net.fetch(pathToFileURL(join(__dirname, '../renderer', file)).toString()); + }); + + const win = new BrowserWindow({ + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + }, + }); + + win.loadURL('my-app://renderer/'); +}); +``` + +In `my-app://renderer/sign-in`, `my-app` is the scheme, `renderer` is the host, `my-app://renderer` is the origin, and `/sign-in` is the path. If your renderer uses path-based routing, serve every route from the same origin and fall back to your renderer entrypoint as needed. + +```tsx +// renderer.tsx +import { ClerkProvider } from '@clerk/electron/react'; + +{/* ... */}; +``` + +## Support + +For help, visit our [support page](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_electron). + +## Contributing + +We're open to all community contributions! If you'd like to contribute in any way, please read [our contribution guidelines](https://github.com/clerk/javascript/blob/main/docs/CONTRIBUTING.md) and [code of conduct](https://github.com/clerk/javascript/blob/main/docs/CODE_OF_CONDUCT.md). + +## Security + +`@clerk/electron` follows good practices of security, but 100% security cannot be assured. + +`@clerk/electron` is provided **"as is"** without any **warranty**. Use at your own risk. + +_For more information and to report security issues, please refer to our [security documentation](https://github.com/clerk/javascript/blob/main/docs/SECURITY.md)._ + +## License + +This project is licensed under the **MIT license**. + +See [LICENSE](https://github.com/clerk/javascript/blob/main/packages/electron/LICENSE) for more information. diff --git a/packages/electron/package.json b/packages/electron/package.json new file mode 100644 index 00000000000..5347bcfed38 --- /dev/null +++ b/packages/electron/package.json @@ -0,0 +1,121 @@ +{ + "name": "@clerk/electron", + "version": "0.0.0", + "description": "Clerk SDK for Electron", + "keywords": [ + "clerk", + "electron", + "auth", + "authentication", + "session", + "jwt", + "desktop" + ], + "homepage": "https://clerk.com/", + "bugs": { + "url": "https://github.com/clerk/javascript/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/clerk/javascript.git", + "directory": "packages/electron" + }, + "license": "MIT", + "author": "Clerk", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./preload": { + "import": { + "types": "./dist/types/preload/index.d.ts", + "default": "./dist/esm/preload/index.js" + }, + "require": { + "types": "./dist/types/preload/index.d.ts", + "default": "./dist/cjs/preload/index.js" + } + }, + "./storage": { + "import": { + "types": "./dist/types/storage/index.d.ts", + "default": "./dist/esm/storage/index.js" + }, + "require": { + "types": "./dist/types/storage/index.d.ts", + "default": "./dist/cjs/storage/index.js" + } + }, + "./react": { + "import": { + "types": "./dist/types/react/index.d.ts", + "default": "./dist/esm/react/index.js" + }, + "require": { + "types": "./dist/types/react/index.d.ts", + "default": "./dist/cjs/react/index.js" + } + }, + "./package.json": "./package.json" + }, + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist", + "preload" + ], + "scripts": { + "build": "tsup", + "build:declarations": "tsc -p tsconfig.declarations.json", + "clean": "rimraf ./dist", + "dev": "tsup --watch", + "format": "node ../../scripts/format-package.mjs", + "format:check": "node ../../scripts/format-package.mjs --check", + "lint": "eslint src", + "lint:attw": "attw --pack . --profile node16 --ignore-rules unexpected-module-syntax", + "lint:publint": "publint", + "test": "vitest run", + "test:ci": "vitest run --maxWorkers=70%", + "test:watch": "vitest" + }, + "dependencies": { + "@clerk/clerk-js": "workspace:^", + "@clerk/react": "workspace:^", + "@clerk/ui": "workspace:^", + "tslib": "catalog:repo" + }, + "devDependencies": { + "@types/node": "^22.19.17", + "electron": "^39.2.6", + "electron-store": "^8.2.0" + }, + "peerDependencies": { + "electron": ">=28", + "electron-store": "^8.2.0", + "react": "catalog:peer-react", + "react-dom": "catalog:peer-react" + }, + "peerDependenciesMeta": { + "electron-store": { + "optional": true + }, + "react-dom": { + "optional": true + } + }, + "engines": { + "node": ">=20.9.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/electron/src/global.d.ts b/packages/electron/src/global.d.ts new file mode 100644 index 00000000000..6c762af29a5 --- /dev/null +++ b/packages/electron/src/global.d.ts @@ -0,0 +1,13 @@ +import type { TokenCache } from './shared/types'; + +declare const PACKAGE_NAME: string; +declare const PACKAGE_VERSION: string; +declare const __DEV__: boolean; + +declare global { + interface Window { + __clerk_internal_electron?: { + tokenCache: TokenCache; + }; + } +} diff --git a/packages/electron/src/index.ts b/packages/electron/src/index.ts new file mode 100644 index 00000000000..ea95302fb59 --- /dev/null +++ b/packages/electron/src/index.ts @@ -0,0 +1 @@ +export { setupMain } from './main/setup-main'; diff --git a/packages/electron/src/main/__tests__/ipc-handlers.test.ts b/packages/electron/src/main/__tests__/ipc-handlers.test.ts new file mode 100644 index 00000000000..c57c98220f4 --- /dev/null +++ b/packages/electron/src/main/__tests__/ipc-handlers.test.ts @@ -0,0 +1,63 @@ +import { ipcMain } from 'electron'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { TOKEN_CACHE_CHANNELS } from '../../shared/ipc'; +import type { TokenStorage } from '../../shared/types'; +import { setupTokenCacheIpcHandlers } from '../ipc-handlers'; + +const ipcEvent = {} as Electron.IpcMainInvokeEvent; + +vi.mock('electron', () => ({ + ipcMain: { + handle: vi.fn(), + removeHandler: vi.fn(), + }, +})); + +describe('setupTokenCacheIpcHandlers', () => { + const storage: TokenStorage = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('registers token cache IPC handlers', () => { + setupTokenCacheIpcHandlers(storage); + + expect(ipcMain.handle).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.getToken, expect.any(Function)); + expect(ipcMain.handle).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.saveToken, expect.any(Function)); + expect(ipcMain.handle).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.clearToken, expect.any(Function)); + }); + + it('delegates token operations to the storage adapter', async () => { + vi.mocked(storage.getItem).mockResolvedValue('jwt'); + + setupTokenCacheIpcHandlers(storage); + + const getTokenHandler = vi.mocked(ipcMain.handle).mock.calls[0][1]; + const saveTokenHandler = vi.mocked(ipcMain.handle).mock.calls[1][1]; + const clearTokenHandler = vi.mocked(ipcMain.handle).mock.calls[2][1]; + + await expect(getTokenHandler(ipcEvent, 'token-key')).resolves.toBe('jwt'); + await saveTokenHandler(ipcEvent, 'token-key', 'jwt'); + await clearTokenHandler(ipcEvent, 'token-key'); + + expect(storage.getItem).toHaveBeenCalledWith('token-key'); + expect(storage.setItem).toHaveBeenCalledWith('token-key', 'jwt'); + expect(storage.removeItem).toHaveBeenCalledWith('token-key'); + }); + + it('removes registered handlers on cleanup', () => { + const cleanup = setupTokenCacheIpcHandlers(storage); + + cleanup(); + + expect(ipcMain.removeHandler).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.getToken); + expect(ipcMain.removeHandler).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.saveToken); + expect(ipcMain.removeHandler).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.clearToken); + }); +}); diff --git a/packages/electron/src/main/__tests__/setup-main.test.ts b/packages/electron/src/main/__tests__/setup-main.test.ts new file mode 100644 index 00000000000..32701055806 --- /dev/null +++ b/packages/electron/src/main/__tests__/setup-main.test.ts @@ -0,0 +1,93 @@ +import { ipcMain, protocol } from 'electron'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { TokenStorage } from '../../shared/types'; +import { setupMain } from '../setup-main'; + +vi.mock('electron', () => ({ + ipcMain: { + handle: vi.fn(), + removeHandler: vi.fn(), + }, + protocol: { + registerSchemesAsPrivileged: vi.fn(), + }, +})); + +describe('setupMain', () => { + const missingStorage = {} as Parameters[0]; + const storage: TokenStorage = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('requires a storage adapter', () => { + expect(() => setupMain(missingStorage)).toThrow('setupMain requires a storage adapter'); + }); + + it('sets up token persistence IPC handlers with the provided storage', () => { + setupMain({ storage }); + + expect(ipcMain.handle).toHaveBeenCalledTimes(3); + }); + + it('registers the configured renderer scheme as privileged before app ready', () => { + setupMain({ + storage, + renderer: { + host: 'renderer', + scheme: 'my-app', + }, + }); + + expect(protocol.registerSchemesAsPrivileged).toHaveBeenCalledWith([ + { + scheme: 'my-app', + privileges: { + corsEnabled: true, + secure: true, + standard: true, + stream: true, + supportFetchAPI: true, + }, + }, + ]); + }); + + it('requires renderer.scheme to be a scheme name, not a URL', () => { + expect(() => + setupMain({ + storage, + renderer: { + host: 'renderer', + scheme: 'my-app://', + }, + }), + ).toThrow('renderer.scheme must be a scheme name'); + }); + + it('requires renderer.host to be a host name, not an origin', () => { + expect(() => + setupMain({ + storage, + renderer: { + host: 'my-app://renderer', + scheme: 'my-app', + }, + }), + ).toThrow('renderer.host must be a host name'); + }); + + it('returns a cleanup function for registered handlers', () => { + const clerk = setupMain({ storage }); + + clerk.cleanup(); + + expect(ipcMain.removeHandler).toHaveBeenCalledTimes(3); + }); +}); diff --git a/packages/electron/src/main/ipc-handlers.ts b/packages/electron/src/main/ipc-handlers.ts new file mode 100644 index 00000000000..5114729404b --- /dev/null +++ b/packages/electron/src/main/ipc-handlers.ts @@ -0,0 +1,24 @@ +import { ipcMain } from 'electron'; + +import { TOKEN_CACHE_CHANNELS } from '../shared/ipc'; +import type { TokenStorage } from '../shared/types'; + +export function setupTokenCacheIpcHandlers(storage: TokenStorage): () => void { + ipcMain.handle(TOKEN_CACHE_CHANNELS.getToken, (_event, key: string) => { + return storage.getItem(key); + }); + + ipcMain.handle(TOKEN_CACHE_CHANNELS.saveToken, (_event, key: string, value: string) => { + return storage.setItem(key, value); + }); + + ipcMain.handle(TOKEN_CACHE_CHANNELS.clearToken, (_event, key: string) => { + return storage.removeItem(key); + }); + + return () => { + ipcMain.removeHandler(TOKEN_CACHE_CHANNELS.getToken); + ipcMain.removeHandler(TOKEN_CACHE_CHANNELS.saveToken); + ipcMain.removeHandler(TOKEN_CACHE_CHANNELS.clearToken); + }; +} diff --git a/packages/electron/src/main/setup-main.ts b/packages/electron/src/main/setup-main.ts new file mode 100644 index 00000000000..aef67fda011 --- /dev/null +++ b/packages/electron/src/main/setup-main.ts @@ -0,0 +1,51 @@ +import { protocol } from 'electron'; + +import type { SetupMainOptions, SetupMainReturn } from '../shared/types'; +import { setupTokenCacheIpcHandlers } from './ipc-handlers'; + +function assertValidRendererOriginConfig(renderer: NonNullable): void { + if (renderer.scheme.includes(':') || renderer.scheme.includes('/')) { + throw new Error( + 'Clerk: renderer.scheme must be a scheme name like "my-app", not a URL or protocol like "my-app://".', + ); + } + + if (renderer.host.includes(':') || renderer.host.includes('/')) { + throw new Error( + 'Clerk: renderer.host must be a host name like "renderer", not a URL or origin like "my-app://renderer".', + ); + } +} + +export function setupMain(options: SetupMainOptions): SetupMainReturn { + if (!options.storage) { + throw new Error( + 'Clerk: setupMain requires a storage adapter. Pass setupMain({ storage: storage() }) from @clerk/electron/storage, or provide a custom storage adapter.', + ); + } + + const cleanupTokenPersistence = setupTokenCacheIpcHandlers(options.storage); + + if (options.renderer) { + assertValidRendererOriginConfig(options.renderer); + + protocol.registerSchemesAsPrivileged([ + { + scheme: options.renderer.scheme, + privileges: { + standard: true, + secure: true, + supportFetchAPI: true, + corsEnabled: true, + stream: true, + }, + }, + ]); + } + + return { + cleanup() { + cleanupTokenPersistence(); + }, + }; +} diff --git a/packages/electron/src/preload/__tests__/index.test.ts b/packages/electron/src/preload/__tests__/index.test.ts new file mode 100644 index 00000000000..28c329ed7b4 --- /dev/null +++ b/packages/electron/src/preload/__tests__/index.test.ts @@ -0,0 +1,71 @@ +import { contextBridge, ipcRenderer } from 'electron'; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { TOKEN_CACHE_CHANNELS } from '../../shared/ipc'; +import { setupPreload } from '../index'; + +vi.mock('electron', () => ({ + contextBridge: { + exposeInMainWorld: vi.fn(), + }, + ipcRenderer: { + invoke: vi.fn(), + }, +})); + +describe('setupPreload', () => { + const originalContextIsolated = process.contextIsolated; + + beforeEach(() => { + vi.clearAllMocks(); + Object.defineProperty(process, 'contextIsolated', { configurable: true, value: true }); + vi.stubGlobal('window', {}); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + afterAll(() => { + Object.defineProperty(process, 'contextIsolated', { configurable: true, value: originalContextIsolated }); + }); + + it('exposes the Clerk Electron bridge through contextBridge when context isolation is enabled', () => { + setupPreload(); + + expect(contextBridge.exposeInMainWorld).toHaveBeenCalledWith('__clerk_internal_electron', { + tokenCache: { + getToken: expect.any(Function), + saveToken: expect.any(Function), + clearToken: expect.any(Function), + }, + }); + }); + + it('exposes the Clerk Electron bridge on window when context isolation is disabled', () => { + Object.defineProperty(process, 'contextIsolated', { configurable: true, value: false }); + + setupPreload(); + + expect(window.__clerk_internal_electron?.tokenCache).toEqual({ + getToken: expect.any(Function), + saveToken: expect.any(Function), + clearToken: expect.any(Function), + }); + }); + + it('forwards token cache calls over IPC', async () => { + setupPreload(); + + const bridge = vi.mocked(contextBridge.exposeInMainWorld).mock + .calls[0][1] as typeof window.__clerk_internal_electron; + + await bridge?.tokenCache.getToken('token-key'); + await bridge?.tokenCache.saveToken('token-key', 'jwt'); + await bridge?.tokenCache.clearToken('token-key'); + + expect(ipcRenderer.invoke).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.getToken, 'token-key'); + expect(ipcRenderer.invoke).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.saveToken, 'token-key', 'jwt'); + expect(ipcRenderer.invoke).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.clearToken, 'token-key'); + }); +}); diff --git a/packages/electron/src/preload/index.ts b/packages/electron/src/preload/index.ts new file mode 100644 index 00000000000..8b9903b1c4d --- /dev/null +++ b/packages/electron/src/preload/index.ts @@ -0,0 +1,18 @@ +import { contextBridge, ipcRenderer } from 'electron'; + +import { TOKEN_CACHE_CHANNELS } from '../shared/ipc'; +import type { TokenCache } from '../shared/types'; + +export function setupPreload(): void { + const tokenCache: TokenCache = { + getToken: key => ipcRenderer.invoke(TOKEN_CACHE_CHANNELS.getToken, key), + saveToken: (key, value) => ipcRenderer.invoke(TOKEN_CACHE_CHANNELS.saveToken, key, value), + clearToken: key => ipcRenderer.invoke(TOKEN_CACHE_CHANNELS.clearToken, key), + }; + + if (process.contextIsolated) { + contextBridge.exposeInMainWorld('__clerk_internal_electron', { tokenCache }); + } else { + window.__clerk_internal_electron = { tokenCache }; + } +} diff --git a/packages/electron/src/react/__tests__/ClerkProvider.test.tsx b/packages/electron/src/react/__tests__/ClerkProvider.test.tsx new file mode 100644 index 00000000000..1aa7cdb90ce --- /dev/null +++ b/packages/electron/src/react/__tests__/ClerkProvider.test.tsx @@ -0,0 +1,111 @@ +import { renderToStaticMarkup } from 'react-dom/server'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { ClerkProvider } from '../index'; + +let capturedProviderProps: Record | null = null; +let beforeRequest: + | ((request: { credentials?: RequestCredentials; headers?: Headers; url?: URL }) => Promise) + | null = null; +let afterResponse: ((request: unknown, response: Response) => Promise) | null = null; + +const clerkConstructor = vi.hoisted(() => vi.fn()); + +vi.mock('@clerk/clerk-js', () => ({ + Clerk: class MockClerk { + constructor(publishableKey: string) { + clerkConstructor(publishableKey); + } + + __internal_onBeforeRequest(cb: typeof beforeRequest) { + beforeRequest = cb; + } + + __internal_onAfterResponse(cb: typeof afterResponse) { + afterResponse = cb; + } + }, +})); + +vi.mock('@clerk/react/internal', () => ({ + InternalClerkProvider: (props: Record) => { + capturedProviderProps = props; + return
{props.children as React.ReactNode}
; + }, +})); + +vi.mock('@clerk/ui', () => ({ + ui: { ClerkUI: 'mock-ui' }, +})); + +describe('Electron ClerkProvider', () => { + const tokenCache = { + clearToken: vi.fn(), + getToken: vi.fn(), + saveToken: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + capturedProviderProps = null; + beforeRequest = null; + afterResponse = null; + vi.stubGlobal('window', { + __clerk_internal_electron: { + tokenCache, + }, + }); + }); + + it('renders React ClerkProvider with Electron defaults', () => { + renderToStaticMarkup( + + App + , + ); + + expect(clerkConstructor).toHaveBeenCalledWith('pk_test_provider'); + expect(capturedProviderProps).toMatchObject({ + publishableKey: 'pk_test_provider', + signInUrl: '/sign-in', + standardBrowser: false, + ui: { ClerkUI: 'mock-ui' }, + }); + expect(capturedProviderProps?.Clerk).toBeDefined(); + expect(capturedProviderProps?.__internal_nativeOAuthHandler).toBeUndefined(); + }); + + it('adds native request params and Authorization from the Electron token cache', async () => { + tokenCache.getToken.mockResolvedValue('client-jwt'); + renderToStaticMarkup(App); + + const request = { + headers: new Headers(), + url: new URL('https://api.clerk.test/v1/client'), + }; + await beforeRequest?.(request); + + expect(request.credentials).toBe('omit'); + expect(request.url.searchParams.get('_is_native')).toBe('1'); + expect(request.headers.get('Authorization')).toBe('Bearer client-jwt'); + expect(tokenCache.getToken).toHaveBeenCalledWith('__clerk_client_jwt'); + }); + + it('stores Authorization response headers in the Electron token cache', async () => { + renderToStaticMarkup(App); + + await afterResponse?.( + {}, + new Response(null, { + headers: { + Authorization: 'Bearer updated-client-jwt', + }, + }), + ); + + expect(tokenCache.saveToken).toHaveBeenCalledWith('__clerk_client_jwt', 'updated-client-jwt'); + }); +}); diff --git a/packages/electron/src/react/create-clerk-instance.ts b/packages/electron/src/react/create-clerk-instance.ts new file mode 100644 index 00000000000..a56a9f6d850 --- /dev/null +++ b/packages/electron/src/react/create-clerk-instance.ts @@ -0,0 +1,40 @@ +import { Clerk } from '@clerk/clerk-js'; + +const CLERK_CLIENT_JWT_KEY = '__clerk_client_jwt'; + +type ClerkInstance = InstanceType; + +let cached: { instance: ClerkInstance; publishableKey: string } | null = null; + +export function createClerkInstance(publishableKey: string): ClerkInstance { + if (cached?.publishableKey === publishableKey) { + return cached.instance; + } + + const clerk = new Clerk(publishableKey); + + clerk.__internal_onBeforeRequest(async request => { + request.credentials = 'omit'; + request.url?.searchParams.append('_is_native', '1'); + + const token = await window.__clerk_internal_electron?.tokenCache.getToken(CLERK_CLIENT_JWT_KEY); + if (token) { + const headers = new Headers(request.headers); + headers.set('Authorization', `Bearer ${token}`); + request.headers = headers; + } + }); + + clerk.__internal_onAfterResponse(async (_request, response) => { + const authorization = (response as Response).headers.get('Authorization'); + if (!authorization) { + return; + } + + const token = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : authorization; + await window.__clerk_internal_electron?.tokenCache.saveToken(CLERK_CLIENT_JWT_KEY, token); + }); + + cached = { instance: clerk, publishableKey }; + return clerk; +} diff --git a/packages/electron/src/react/index.tsx b/packages/electron/src/react/index.tsx new file mode 100644 index 00000000000..ddfa6de5607 --- /dev/null +++ b/packages/electron/src/react/index.tsx @@ -0,0 +1,35 @@ +import type { ClerkProviderProps as ReactClerkProviderProps } from '@clerk/react'; +import { InternalClerkProvider as ReactClerkProvider } from '@clerk/react/internal'; +import { ui } from '@clerk/ui'; +import type { ReactNode } from 'react'; + +import { createClerkInstance } from './create-clerk-instance'; + +export type ClerkProviderProps = Omit< + ReactClerkProviderProps, + 'Clerk' | 'children' | 'publishableKey' | 'standardBrowser' | 'ui' +> & { + children: ReactNode; + /** + * Your Clerk publishable key, available in the Clerk Dashboard. + */ + publishableKey: string; +}; + +export function ClerkProvider({ children, publishableKey, ...props }: ClerkProviderProps): JSX.Element { + const clerk = createClerkInstance(publishableKey); + + return ( + + {children} + + ); +} + +export * from '@clerk/react'; diff --git a/packages/electron/src/shared/ipc.ts b/packages/electron/src/shared/ipc.ts new file mode 100644 index 00000000000..e50db4bb9c3 --- /dev/null +++ b/packages/electron/src/shared/ipc.ts @@ -0,0 +1,5 @@ +export const TOKEN_CACHE_CHANNELS = { + getToken: 'clerk:token-cache:get', + saveToken: 'clerk:token-cache:save', + clearToken: 'clerk:token-cache:clear', +} as const; diff --git a/packages/electron/src/shared/types.ts b/packages/electron/src/shared/types.ts new file mode 100644 index 00000000000..14aea2c2a70 --- /dev/null +++ b/packages/electron/src/shared/types.ts @@ -0,0 +1,36 @@ +type Awaitable = T | Promise; + +export type TokenStorage = { + getItem: (key: string) => Awaitable; + setItem: (key: string, value: string) => Awaitable; + removeItem: (key: string) => Awaitable; +}; + +export type SetupMainOptions = { + storage: TokenStorage; + /** + * Registers the custom scheme used to serve the Electron renderer from a stable origin. + */ + renderer?: RendererSchemeOptions; +}; + +export type SetupMainReturn = { + cleanup: () => void; +}; + +export type RendererSchemeOptions = { + /** + * Custom scheme used for the renderer origin. + */ + scheme: string; + /** + * Custom host used for the renderer origin. + */ + host: string; +}; + +export type TokenCache = { + getToken: (key: string) => Promise; + saveToken: (key: string, value: string) => Promise; + clearToken: (key: string) => Promise; +}; diff --git a/packages/electron/src/storage/__tests__/index.test.ts b/packages/electron/src/storage/__tests__/index.test.ts new file mode 100644 index 00000000000..aa930e632b3 --- /dev/null +++ b/packages/electron/src/storage/__tests__/index.test.ts @@ -0,0 +1,310 @@ +import { safeStorage } from 'electron'; +import Store from 'electron-store'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { storage } from '../index'; + +const storeGet = vi.fn(); +const storeSet = vi.fn(); +const storeDelete = vi.fn(); + +type AsyncSafeStorage = { + encryptStringAsync: (value: string) => Promise; + decryptStringAsync: (encrypted: Buffer) => Promise<{ result: string; shouldReEncrypt: boolean }>; + isAsyncEncryptionAvailable: () => Promise; +}; + +// `safeStorage` starts empty; each test installs only the methods for the backend it exercises. +// This mirrors reality: Electron < 42 has no async methods at all, so `resolveCipher` only takes the +// async path when both the methods exist and `isAsyncEncryptionAvailable()` confirms it. +vi.mock('electron', () => ({ + safeStorage: {}, +})); + +vi.mock('electron-store', () => ({ + default: vi.fn(() => ({ + get: storeGet, + set: storeSet, + delete: storeDelete, + })), +})); + +const ss = safeStorage as unknown as Record; +const asyncSafeStorage = safeStorage as typeof safeStorage & AsyncSafeStorage; + +/** Installs the synchronous `safeStorage` API. */ +function installSync({ available = true }: { available?: boolean } = {}) { + ss.isEncryptionAvailable = vi.fn(() => available); + ss.encryptString = vi.fn((value: string) => Buffer.from(`enc(${value})`)); + ss.decryptString = vi.fn(); +} + +/** Adds the Electron 42+ asynchronous `safeStorage` API. */ +function installAsync({ available = true }: { available?: boolean } = {}) { + ss.isAsyncEncryptionAvailable = vi.fn(() => Promise.resolve(available)); + ss.encryptStringAsync = vi.fn((value: string) => Promise.resolve(Buffer.from(`enc(${value})`))); + ss.decryptStringAsync = vi.fn(); +} + +beforeEach(() => { + vi.clearAllMocks(); + // Reset the mocked `safeStorage` shape so backend detection starts from a clean slate. + for (const key of Object.keys(ss)) { + delete ss[key]; + } +}); + +describe('storage options', () => { + beforeEach(() => installSync()); + + it('creates an electron-store instance with the default store name', () => { + storage(); + + expect(Store).toHaveBeenCalledWith({ name: 'clerk-tokens' }); + }); + + it('supports a custom store name', () => { + storage({ name: 'custom-clerk-tokens' }); + + expect(Store).toHaveBeenCalledWith({ name: 'custom-clerk-tokens' }); + }); + + it('forwards a custom path as electron-store `cwd`', () => { + storage({ path: '/tmp/clerk' }); + + expect(Store).toHaveBeenCalledWith({ name: 'clerk-tokens', cwd: '/tmp/clerk' }); + }); + + it('omits `cwd` when no path is provided', () => { + storage(); + + expect(Store).toHaveBeenCalledWith(expect.not.objectContaining({ cwd: expect.anything() })); + }); +}); + +describe('getItem', () => { + it('returns null when a token is missing', async () => { + installSync(); + storeGet.mockReturnValue(undefined); + + await expect(storage().getItem('token-key')).resolves.toBeNull(); + }); + + it('returns an unencrypted (raw:) value as-is without decrypting', async () => { + installSync(); + storeGet.mockReturnValue('raw:jwt'); + + await expect(storage().getItem('token-key')).resolves.toBe('jwt'); + expect(safeStorage.decryptString).not.toHaveBeenCalled(); + }); + + it('deletes and returns null for an unrecognized value format', async () => { + installSync(); + storeGet.mockReturnValue('garbage-without-prefix'); + + await expect(storage().getItem('token-key')).resolves.toBeNull(); + expect(storeDelete).toHaveBeenCalledWith('token-key'); + }); + + it('preserves the entry and returns null when no OS encryption is available', async () => { + installSync({ available: false }); + storeGet.mockReturnValue(`enc:${Buffer.from('cipher').toString('base64')}`); + + await expect(storage().getItem('token-key')).resolves.toBeNull(); + expect(storeDelete).not.toHaveBeenCalled(); + }); + + describe('sync backend', () => { + it('decrypts stored tokens', async () => { + installSync(); + storeGet.mockReturnValue(`enc:${Buffer.from('cipher').toString('base64')}`); + vi.mocked(safeStorage.decryptString).mockReturnValue('jwt'); + + await expect(storage().getItem('token-key')).resolves.toBe('jwt'); + expect(safeStorage.decryptString).toHaveBeenCalledWith(Buffer.from('cipher')); + }); + + it('returns null without deleting the entry when decryption fails', async () => { + installSync(); + storeGet.mockReturnValue(`enc:${Buffer.from('cipher').toString('base64')}`); + vi.mocked(safeStorage.decryptString).mockImplementation(() => { + throw new Error('decrypt failed'); + }); + + await expect(storage().getItem('token-key')).resolves.toBeNull(); + expect(storeDelete).not.toHaveBeenCalled(); + }); + }); + + describe('async backend', () => { + it('decrypts stored tokens', async () => { + installSync(); + installAsync(); + storeGet.mockReturnValue(`enc:${Buffer.from('cipher').toString('base64')}`); + vi.mocked(asyncSafeStorage.decryptStringAsync).mockResolvedValue({ result: 'jwt', shouldReEncrypt: false }); + + await expect(storage().getItem('token-key')).resolves.toBe('jwt'); + expect(asyncSafeStorage.decryptStringAsync).toHaveBeenCalledWith(Buffer.from('cipher')); + }); + + it('re-encrypts and re-saves when the OS key has rotated (shouldReEncrypt)', async () => { + installSync(); + installAsync(); + storeGet.mockReturnValue(`enc:${Buffer.from('old-cipher').toString('base64')}`); + vi.mocked(asyncSafeStorage.decryptStringAsync).mockResolvedValue({ result: 'jwt', shouldReEncrypt: true }); + + await expect(storage().getItem('token-key')).resolves.toBe('jwt'); + expect(asyncSafeStorage.encryptStringAsync).toHaveBeenCalledWith('jwt'); + expect(storeSet).toHaveBeenCalledWith('token-key', `enc:${Buffer.from('enc(jwt)').toString('base64')}`); + }); + + it('returns null without deleting the entry when decryption rejects', async () => { + installSync(); + installAsync(); + storeGet.mockReturnValue(`enc:${Buffer.from('cipher').toString('base64')}`); + vi.mocked(asyncSafeStorage.decryptStringAsync).mockRejectedValue(new Error('decrypt failed')); + + await expect(storage().getItem('token-key')).resolves.toBeNull(); + expect(storeDelete).not.toHaveBeenCalled(); + }); + + it('falls back to the sync API (never calling the async crypto) when async encryption is unavailable', async () => { + installSync(); + installAsync({ available: false }); + storeGet.mockReturnValue(`enc:${Buffer.from('cipher').toString('base64')}`); + vi.mocked(safeStorage.decryptString).mockReturnValue('jwt'); + + await expect(storage().getItem('token-key')).resolves.toBe('jwt'); + expect(safeStorage.decryptString).toHaveBeenCalledWith(Buffer.from('cipher')); + // Critical: calling the async crypto while unavailable crashes the process on Electron 42.x. + expect(asyncSafeStorage.decryptStringAsync).not.toHaveBeenCalled(); + }); + }); +}); + +describe('setItem', () => { + it('encrypts tokens before storing them (sync backend)', async () => { + installSync(); + + await storage().setItem('token-key', 'jwt'); + + expect(safeStorage.encryptString).toHaveBeenCalledWith('jwt'); + expect(storeSet).toHaveBeenCalledWith('token-key', `enc:${Buffer.from('enc(jwt)').toString('base64')}`); + }); + + it('encrypts tokens before storing them (async backend)', async () => { + installSync(); + installAsync(); + + await storage().setItem('token-key', 'jwt'); + + expect(asyncSafeStorage.encryptStringAsync).toHaveBeenCalledWith('jwt'); + expect(storeSet).toHaveBeenCalledWith('token-key', `enc:${Buffer.from('enc(jwt)').toString('base64')}`); + }); + + it('falls back to the sync API (never calling the async crypto) when async encryption is unavailable', async () => { + installSync(); + installAsync({ available: false }); + + await storage().setItem('token-key', 'jwt'); + + expect(safeStorage.encryptString).toHaveBeenCalledWith('jwt'); + expect(asyncSafeStorage.encryptStringAsync).not.toHaveBeenCalled(); + expect(storeSet).toHaveBeenCalledWith('token-key', `enc:${Buffer.from('enc(jwt)').toString('base64')}`); + }); + + it('does not persist when no encryption is available and no fallback is configured', async () => { + installSync({ available: false }); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + await storage().setItem('token-key', 'jwt'); + + expect(storeSet).not.toHaveBeenCalled(); + expect(warn).toHaveBeenCalledOnce(); + + warn.mockRestore(); + }); + + it('persists unencrypted when no encryption is available and unencryptedFallback is enabled', async () => { + installSync({ available: false }); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + await storage({ unencryptedFallback: true }).setItem('token-key', 'jwt'); + + expect(storeSet).toHaveBeenCalledWith('token-key', 'raw:jwt'); + expect(warn).toHaveBeenCalledOnce(); + + warn.mockRestore(); + }); + + it('only warns once across repeated saves', async () => { + installSync({ available: false }); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const adapter = storage(); + await adapter.setItem('a', '1'); + await adapter.setItem('b', '2'); + + expect(warn).toHaveBeenCalledOnce(); + + warn.mockRestore(); + }); + + it('does not downgrade to plaintext when encryption is available but encrypt() fails', async () => { + ss.isEncryptionAvailable = vi.fn(() => true); + ss.encryptString = vi.fn(() => { + throw new Error('encrypt failed'); + }); + ss.decryptString = vi.fn(); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + // Even with the fallback enabled, a *failed encrypt* (vs. unavailable encryption) must not be + // persisted in the clear. + await storage({ unencryptedFallback: true }).setItem('token-key', 'jwt'); + + expect(storeSet).not.toHaveBeenCalled(); + expect(warn).toHaveBeenCalledOnce(); + + warn.mockRestore(); + }); + + it('resolves the cipher lazily and retries when it was initially unavailable (e.g. before app ready)', async () => { + // Unavailable on the first probe (pre-`ready`), available afterwards. + ss.isEncryptionAvailable = vi.fn().mockReturnValueOnce(false).mockReturnValue(true); + ss.encryptString = vi.fn((value: string) => Buffer.from(`enc(${value})`)); + ss.decryptString = vi.fn(); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const adapter = storage(); + + await adapter.setItem('token-key', 'jwt'); + expect(storeSet).not.toHaveBeenCalled(); // not persisted while unavailable + + await adapter.setItem('token-key', 'jwt'); + expect(storeSet).toHaveBeenCalledWith('token-key', `enc:${Buffer.from('enc(jwt)').toString('base64')}`); + + warn.mockRestore(); + }); + + it('ignores a partial async surface and uses the sync API', async () => { + // `encryptStringAsync` exists but the rest of the async trio does not — must not take the async + // path (and must not call the async crypto). + ss.encryptStringAsync = vi.fn(); + ss.isEncryptionAvailable = vi.fn(() => true); + ss.encryptString = vi.fn((value: string) => Buffer.from(`enc(${value})`)); + ss.decryptString = vi.fn(); + + await storage().setItem('token-key', 'jwt'); + + expect(safeStorage.encryptString).toHaveBeenCalledWith('jwt'); + expect(asyncSafeStorage.encryptStringAsync).not.toHaveBeenCalled(); + }); +}); + +describe('removeItem', () => { + it('removes stored tokens', async () => { + await storage().removeItem('token-key'); + + expect(storeDelete).toHaveBeenCalledWith('token-key'); + }); +}); diff --git a/packages/electron/src/storage/index.ts b/packages/electron/src/storage/index.ts new file mode 100644 index 00000000000..8dd10f5f95b --- /dev/null +++ b/packages/electron/src/storage/index.ts @@ -0,0 +1,229 @@ +import { safeStorage } from 'electron'; +import Store from 'electron-store'; + +import type { TokenStorage } from '../shared/types'; + +type StorageOptions = { + /** + * The name of the file (without extension) used to persist tokens. + * + * @default 'clerk-tokens' + */ + name?: string; + /** + * The directory in which the token file is stored. Maps to `electron-store`'s `cwd` option. + * When omitted, the OS default user config directory is used. + */ + path?: string; + /** + * When OS-level encryption is unavailable (e.g. a Linux machine without a keyring), tokens are + * not persisted by default, which signs the user out on the next app launch. Set this to `true` + * to instead persist tokens **unencrypted** in that scenario, keeping the user signed in across + * restarts at the cost of storing tokens in plaintext on disk. + * + * @default false + */ + unencryptedFallback?: boolean; +}; + +/** + * Marks a value that was encrypted via {@link safeStorage}; the remainder is base64 ciphertext. + * values will be stored as: `enc:` + */ +const ENCRYPTED_PREFIX = 'enc:'; +/** + * Marks a value persisted unencrypted via the `unencryptedFallback` option. + * values will be stored as: `raw:` + */ +const RAW_PREFIX = 'raw:'; + +type Cipher = { + encrypt: (value: string) => Promise; + /** + * Decrypts a base64 payload. `shouldReEncrypt` is `true` (async backend only) when the OS key + * has rotated and the payload should be re-encrypted with the new key. + */ + decrypt: (payload: string) => Promise<{ value: string; shouldReEncrypt: boolean }>; +}; + +type AsyncSafeStorage = { + encryptStringAsync: (plainText: string) => Promise; + decryptStringAsync: (encrypted: Buffer) => Promise<{ result: string; shouldReEncrypt: boolean }>; + isAsyncEncryptionAvailable: () => Promise; +}; + +const syncCipher: Cipher = { + encrypt: value => Promise.resolve(safeStorage.encryptString(value).toString('base64')), + decrypt: payload => + Promise.resolve({ value: safeStorage.decryptString(Buffer.from(payload, 'base64')), shouldReEncrypt: false }), +}; + +function hasAsyncSafeStorage(storage: typeof safeStorage): storage is typeof safeStorage & AsyncSafeStorage { + const candidate = storage as Partial; + + return ( + typeof candidate.encryptStringAsync === 'function' && + typeof candidate.decryptStringAsync === 'function' && + typeof candidate.isAsyncEncryptionAvailable === 'function' + ); +} + +function createAsyncCipher(storage: AsyncSafeStorage): Cipher { + return { + encrypt: async value => (await storage.encryptStringAsync(value)).toString('base64'), + decrypt: async payload => { + const { result, shouldReEncrypt } = await storage.decryptStringAsync(Buffer.from(payload, 'base64')); + return { value: result, shouldReEncrypt }; + }, + }; +} + +/** + * Resolves the crypto backend to use, or `null` when no OS encryption is currently available. + */ +async function resolveCipher(): Promise { + // Prefer the async API, but only when the full optional function surface exists. + if (hasAsyncSafeStorage(safeStorage)) { + try { + if (await safeStorage.isAsyncEncryptionAvailable()) { + return createAsyncCipher(safeStorage); + } + } catch { + /* fall through to the synchronous API */ + } + } + + // The synchronous API blocks the calling thread on the OS prompt during the first encrypt/decrypt, + if (typeof safeStorage.isEncryptionAvailable === 'function' && safeStorage.isEncryptionAvailable()) { + return syncCipher; + } + + return null; +} + +/** + * Creates a secure {@link TokenStorage} adapter for the Electron main process. + * + * Tokens are persisted with `electron-store` and encrypted at rest using Electron's + * {@link safeStorage} API, which is backed by the OS keystore (Keychain on macOS, DPAPI on + * Windows, libsecret/kwallet on Linux). It uses Electron 42's async `safeStorage` API only when it + * reports itself available (which generally requires a code-signed app) and otherwise falls back to + * the synchronous API. Pass the result to `setupMain({ storage: storage() })`. + * + * Behavior is secure by default: when OS encryption is unavailable the adapter does not persist + * tokens (the user will be signed out on restart) unless {@link StorageOptions.unencryptedFallback} + * is enabled. Undecryptable entries return `null`. + * + * @example + * ```ts + * import { setupMain } from '@clerk/electron'; + * import { storage } from '@clerk/electron/storage'; + * + * setupMain({ storage: storage({ name: 'my-app-tokens' }) }); + * ``` + */ +export function storage(options: StorageOptions = {}): TokenStorage { + const store = new Store>({ + name: options.name ?? 'clerk-tokens', + ...(options.path ? { cwd: options.path } : {}), + }); + + let cachedCipher: Cipher | null = null; + let resolving: Promise | null = null; + const getCipher = (): Promise => { + if (cachedCipher) { + return Promise.resolve(cachedCipher); + } + resolving ??= resolveCipher().then(resolved => { + resolving = null; + if (resolved) { + cachedCipher = resolved; + } + return resolved; + }); + return resolving; + }; + + let warned = false; + const warnOnce = (message: string) => { + if (warned) { + return; + } + warned = true; + console.warn(message); + }; + + return { + async getItem(key) { + const stored = store.get(key); + + if (!stored) { + return null; + } + + if (stored.startsWith(RAW_PREFIX)) { + return stored.slice(RAW_PREFIX.length); + } + + if (stored.startsWith(ENCRYPTED_PREFIX)) { + const cipher = await getCipher(); + + // No usable OS encryption, preserve entry. + if (!cipher) { + return null; + } + + const payload = stored.slice(ENCRYPTED_PREFIX.length); + + try { + const { value, shouldReEncrypt } = await cipher.decrypt(payload); + + if (shouldReEncrypt) { + // OS key has rotated, persist with new value + try { + store.set(key, ENCRYPTED_PREFIX + (await cipher.encrypt(value))); + } catch { + // keep the existing payload; it still decrypts for now + } + } + + return value; + } catch { + // Decryption failed, preserve the entry, new write on next sign-in. + return null; + } + } + + // Unknown or unrecognized format, drop the entry so we don't repeatedly fail on it. + store.delete(key); + return null; + }, + async setItem(key, value) { + const cipher = await getCipher(); + + if (!cipher) { + if (options.unencryptedFallback) { + warnOnce( + 'Clerk: OS encryption is unavailable; falling back to unencrypted storage. Session tokens are being stored unencrypted on local disk.', + ); + store.set(key, RAW_PREFIX + value); + } else { + warnOnce( + 'Clerk: OS encryption is unavailable and unencryptedFallback is not enabled, so tokens are not being persisted. The user will be signed out on the next launch. Pass `storage({ unencryptedFallback: true })` to persist unencrypted (less secure).', + ); + } + return; + } + + try { + store.set(key, ENCRYPTED_PREFIX + (await cipher.encrypt(value))); + } catch { + // Encryption is available but encryption failed + warnOnce('Clerk: failed to encrypt the session token; it was not persisted.'); + } + }, + removeItem(key) { + store.delete(key); + }, + }; +} diff --git a/packages/electron/tsconfig.declarations.json b/packages/electron/tsconfig.declarations.json new file mode 100644 index 00000000000..860b72f67d1 --- /dev/null +++ b/packages/electron/tsconfig.declarations.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "skipLibCheck": true, + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + "declarationMap": true, + "sourceMap": false, + "declarationDir": "./dist/types" + }, + "exclude": ["**/__tests__/**/*"] +} diff --git a/packages/electron/tsconfig.json b/packages/electron/tsconfig.json new file mode 100644 index 00000000000..3969b0a2146 --- /dev/null +++ b/packages/electron/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2019", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "preserve", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "declaration": true, + "declarationDir": "dist/types", + "declarationMap": true, + "emitDeclarationOnly": true + }, + "include": ["src"], + "exclude": ["src/**/__tests__/**"] +} diff --git a/packages/electron/tsup.config.ts b/packages/electron/tsup.config.ts new file mode 100644 index 00000000000..a8beef878b5 --- /dev/null +++ b/packages/electron/tsup.config.ts @@ -0,0 +1,38 @@ +import type { Options } from 'tsup'; +import { defineConfig } from 'tsup'; + +import { runAfterLast } from '../../scripts/utils'; +import { name, version } from './package.json'; + +export default defineConfig(overrideOptions => { + const isWatch = !!overrideOptions.watch; + const shouldPublish = !!overrideOptions.env?.publish; + + const common: Options = { + entry: ['./src/index.ts', './src/preload/index.ts', './src/react/index.tsx', './src/storage/index.ts'], + bundle: true, + clean: true, + minify: false, + sourcemap: true, + legacyOutput: true, + treeshake: true, + define: { + PACKAGE_NAME: `"${name}"`, + PACKAGE_VERSION: `"${version}"`, + __DEV__: `${isWatch}`, + }, + }; + + const esm: Options = { + ...common, + format: 'esm', + }; + + const cjs: Options = { + ...common, + format: 'cjs', + outDir: './dist/cjs', + }; + + return runAfterLast(['pnpm build:declarations', shouldPublish && 'pkglab pub --ping'])(esm, cjs); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b02d0e16fe2..4f42f04b13a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -515,6 +515,37 @@ importers: specifier: ^5.10.0 version: 5.10.0 + packages/electron: + dependencies: + '@clerk/clerk-js': + specifier: workspace:^ + version: link:../clerk-js + '@clerk/react': + specifier: workspace:^ + version: link:../react + '@clerk/ui': + specifier: workspace:^ + version: link:../ui + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + tslib: + specifier: catalog:repo + version: 2.8.1 + devDependencies: + '@types/node': + specifier: ^22.19.17 + version: 22.19.17 + electron: + specifier: ^39.2.6 + version: 39.8.10 + electron-store: + specifier: ^8.2.0 + version: 8.2.0 + packages/expo: dependencies: '@clerk/clerk-js': @@ -2303,6 +2334,10 @@ packages: resolution: {integrity: sha512-NKBGBSIKUG584qrS1tyxVpX/AKJKQw5HgjYEnPLC0QsTw79JrGn+qUr8CXFb955Iy7GUdiiUv1rJ6JBGvaKb6w==} engines: {node: '>=18'} + '@electron/get@2.0.3': + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -5438,6 +5473,10 @@ packages: '@swc/helpers@0.5.21': resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + '@tailwindcss/node@4.3.0': resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} @@ -5752,6 +5791,9 @@ packages: '@types/bonjour@3.5.13': resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -5824,6 +5866,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} @@ -5854,6 +5899,9 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -5916,6 +5964,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/retry@0.12.2': resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} @@ -6898,6 +6949,10 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} + atomically@1.7.0: + resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} + engines: {node: '>=10.12.0'} + autoprefixer@10.5.0: resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} @@ -7121,6 +7176,10 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + borsh@0.7.0: resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} @@ -7246,6 +7305,14 @@ packages: resolution: {integrity: sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==} engines: {node: ^16.14.0 || >=18.0.0} + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + cachedir@2.4.0: resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} engines: {node: '>=6'} @@ -7497,6 +7564,9 @@ packages: resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} engines: {node: '>=6'} + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -7655,6 +7725,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + conf@10.2.0: + resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==} + engines: {node: '>=12'} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -8010,6 +8084,10 @@ packages: de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debounce-fn@4.0.0: + resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==} + engines: {node: '>=10'} + debounce@1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} @@ -8078,6 +8156,10 @@ packages: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dedent@1.7.2: resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: @@ -8124,6 +8206,10 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -8276,6 +8362,10 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} + dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + dotenv-expand@11.0.7: resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} engines: {node: '>=12'} @@ -8350,9 +8440,17 @@ packages: engines: {node: '>=0.10.0'} hasBin: true + electron-store@8.2.0: + resolution: {integrity: sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw==} + electron-to-chromium@1.5.331: resolution: {integrity: sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==} + electron@39.8.10: + resolution: {integrity: sha512-zbYtGPYUI7PzqLAzkk21Rk6j67WN0hxn0Mq/njErZo1d0HSf33is4f8ICI5fMLy5vYe0JtCtM5sYunNOaochSQ==} + engines: {node: '>= 12.20.55'} + hasBin: true + emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} @@ -8503,6 +8601,9 @@ packages: es-toolkit@1.47.0: resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==} + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + es6-promise@4.2.8: resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} @@ -9519,6 +9620,10 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} @@ -9567,6 +9672,10 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -9838,6 +9947,10 @@ packages: resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} engines: {node: '>=0.10'} + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -10539,6 +10652,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@7.0.3: + resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==} + json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} @@ -10972,6 +11088,10 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -11071,6 +11191,10 @@ packages: marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -11469,6 +11593,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-fn@3.1.0: + resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} + engines: {node: '>=8'} + mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} @@ -11477,6 +11605,14 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -11790,6 +11926,10 @@ packages: resolution: {integrity: sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==} engines: {node: '>=4'} + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + npm-package-arg@11.0.3: resolution: {integrity: sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==} engines: {node: ^16.14.0 || >=18.0.0} @@ -12046,6 +12186,10 @@ packages: peerDependencies: oxc-parser: '>=0.98.0' + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + p-event@5.0.1: resolution: {integrity: sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -12346,6 +12490,10 @@ packages: pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + pkglab-darwin-arm64@0.17.1: resolution: {integrity: sha512-7cy7ck7iQW6xYDBkV3NHW1wz4bub9sikFkrMj11AT/1PXVYFDhI7SaaZ6jxQgL3Mcvj34rQlUXKyVWrAzhrUDw==} engines: {node: '>=18'} @@ -12876,6 +13024,10 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + quick-lru@6.1.2: resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} engines: {node: '>=12'} @@ -13173,6 +13325,9 @@ packages: reselect@5.2.0: resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + resolve-from@3.0.0: resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} engines: {node: '>=4'} @@ -13211,6 +13366,9 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + restore-cursor@2.0.0: resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} engines: {node: '>=4'} @@ -13276,6 +13434,10 @@ packages: engines: {node: 20 || >=22} hasBin: true + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + rolldown-plugin-dts@0.16.12: resolution: {integrity: sha512-9dGjm5oqtKcbZNhpzyBgb8KrYiU616A7IqcFWG7Msp1RKAXQ/hapjivRg+g5IYWSiFhnk3OKYV5T4Ft1t8Cczg==} engines: {node: '>=20.18.0'} @@ -13428,6 +13590,9 @@ packages: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + semver-regex@4.0.5: resolution: {integrity: sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==} engines: {node: '>=12'} @@ -13453,6 +13618,10 @@ packages: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + serialize-javascript@7.0.5: resolution: {integrity: sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==} engines: {node: '>=20.0.0'} @@ -13738,6 +13907,9 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + srvx@0.10.1: resolution: {integrity: sha512-A//xtfak4eESMWWydSRFUVvCTQbSwivnGCEf8YGPe2eHU0+Z6znfUTCPF0a7oV3sObSOcrXHlL6Bs9vVctfXdg==} engines: {node: '>=20.16.0'} @@ -14008,6 +14180,10 @@ packages: suf-log@2.5.3: resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==} + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + superagent@8.1.2: resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} engines: {node: '>=6.4.0 <13 || >=14'} @@ -14408,6 +14584,10 @@ packages: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + type-fest@0.16.0: resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} engines: {node: '>=10'} @@ -17140,6 +17320,20 @@ snapshots: dependencies: '@edge-runtime/primitives': 6.0.0 + '@electron/get@2.0.3': + dependencies: + debug: 4.4.3(supports-color@8.1.1) + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 7.7.4 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -21094,6 +21288,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + '@tailwindcss/node@4.3.0': dependencies: '@jridgewell/remapping': 2.3.5 @@ -21477,6 +21675,13 @@ snapshots: dependencies: '@types/node': 22.19.17 + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 22.19.17 + '@types/responselike': 1.0.3 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -21570,6 +21775,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/http-cache-semantics@4.2.0': {} + '@types/http-errors@2.0.5': {} '@types/http-proxy@1.17.17': @@ -21601,6 +21808,10 @@ snapshots: dependencies: '@types/node': 22.19.17 + '@types/keyv@3.1.4': + dependencies: + '@types/node': 22.19.17 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -21659,6 +21870,10 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/responselike@1.0.3': + dependencies: + '@types/node': 22.19.17 + '@types/retry@0.12.2': {} '@types/semver@7.7.1': {} @@ -22932,6 +23147,8 @@ snapshots: atomic-sleep@1.0.0: {} + atomically@1.7.0: {} + autoprefixer@10.5.0(postcss@8.5.13): dependencies: browserslist: 4.28.2 @@ -23207,6 +23424,9 @@ snapshots: boolbase@1.0.0: {} + boolean@3.2.0: + optional: true + borsh@0.7.0: dependencies: bn.js: 5.2.3 @@ -23374,6 +23594,18 @@ snapshots: tar: 7.5.11 unique-filename: 3.0.0 + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + cachedir@2.4.0: {} call-bind-apply-helpers@1.0.2: @@ -23651,6 +23883,10 @@ snapshots: kind-of: 6.0.3 shallow-clone: 3.0.1 + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + clone@1.0.4: {} clsx@1.2.1: {} @@ -23772,6 +24008,19 @@ snapshots: concat-map@0.0.1: {} + conf@10.2.0: + dependencies: + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + atomically: 1.7.0 + debounce-fn: 4.0.0 + dot-prop: 6.0.1 + env-paths: 2.2.1 + json-schema-typed: 7.0.3 + onetime: 5.1.2 + pkg-up: 3.1.0 + semver: 7.7.4 + confbox@0.1.8: {} confbox@0.2.4: {} @@ -24188,6 +24437,10 @@ snapshots: de-indent@1.0.2: {} + debounce-fn@4.0.0: + dependencies: + mimic-fn: 3.1.0 + debounce@1.2.1: {} debug@2.6.9: @@ -24231,6 +24484,10 @@ snapshots: decode-uri-component@0.2.2: {} + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + dedent@1.7.2(babel-plugin-macros@3.1.0): optionalDependencies: babel-plugin-macros: 3.1.0 @@ -24284,6 +24541,8 @@ snapshots: dependencies: clone: 1.0.4 + defer-to-connect@2.0.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -24423,6 +24682,10 @@ snapshots: dependencies: is-obj: 2.0.0 + dot-prop@6.0.1: + dependencies: + is-obj: 2.0.0 + dotenv-expand@11.0.7: dependencies: dotenv: 16.6.1 @@ -24482,8 +24745,21 @@ snapshots: dependencies: jake: 10.9.4 + electron-store@8.2.0: + dependencies: + conf: 10.2.0 + type-fest: 2.19.0 + electron-to-chromium@1.5.331: {} + electron@39.8.10: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 22.19.17 + extract-zip: 2.0.1(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + emoji-regex-xs@1.0.0: {} emoji-regex@10.6.0: {} @@ -24698,6 +24974,9 @@ snapshots: es-toolkit@1.47.0: {} + es6-error@4.1.1: + optional: true + es6-promise@4.2.8: {} es6-promisify@5.0.0: @@ -26084,6 +26363,16 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.7.4 + serialize-error: 7.0.1 + optional: true + global-directory@4.0.1: dependencies: ini: 4.1.1 @@ -26139,6 +26428,20 @@ snapshots: gopd@1.2.0: {} + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + graceful-fs@4.2.11: {} graphql@16.13.2: {} @@ -26531,6 +26834,11 @@ snapshots: jsprim: 2.0.2 sshpk: 1.18.0 + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -27223,6 +27531,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@7.0.3: {} + json-schema-typed@8.0.2: {} json-schema@0.4.0: {} @@ -27638,6 +27948,8 @@ snapshots: dependencies: tslib: 2.8.1 + lowercase-keys@2.0.0: {} + lru-cache@10.4.3: {} lru-cache@11.3.5: {} @@ -27739,6 +28051,11 @@ snapshots: marky@1.3.0: {} + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + math-intrinsics@1.1.0: {} md5-file@3.2.3: @@ -28626,10 +28943,16 @@ snapshots: mimic-fn@2.1.0: {} + mimic-fn@3.1.0: {} + mimic-fn@4.0.0: {} mimic-function@5.0.1: {} + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + min-indent@1.0.1: {} minimalistic-assert@1.0.1: {} @@ -29087,6 +29410,8 @@ snapshots: query-string: 5.1.1 sort-keys: 2.0.0 + normalize-url@6.1.0: {} + npm-package-arg@11.0.3: dependencies: hosted-git-info: 7.0.2 @@ -29596,6 +29921,8 @@ snapshots: magic-regexp: 0.10.0 oxc-parser: 0.128.0 + p-cancelable@2.1.1: {} + p-event@5.0.1: dependencies: p-timeout: 5.1.0 @@ -29869,6 +30196,10 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + pkg-up@3.1.0: + dependencies: + find-up: 3.0.0 + pkglab-darwin-arm64@0.17.1: optional: true @@ -30293,6 +30624,8 @@ snapshots: quick-format-unescaped@4.0.4: {} + quick-lru@5.1.1: {} + quick-lru@6.1.2: {} radix3@1.1.2: {} @@ -30736,6 +31069,8 @@ snapshots: reselect@5.2.0: {} + resolve-alpn@1.2.1: {} + resolve-from@3.0.0: optional: true @@ -30769,6 +31104,10 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + restore-cursor@2.0.0: dependencies: onetime: 2.0.1 @@ -30839,6 +31178,16 @@ snapshots: glob: 11.0.3 package-json-from-dist: 1.0.1 + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + rolldown-plugin-dts@0.16.12(rolldown@1.0.0-beta.47(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(typescript@5.8.3)(vue-tsc@3.1.4(typescript@5.8.3)): dependencies: '@babel/generator': 7.29.1 @@ -31047,6 +31396,9 @@ snapshots: '@types/node-forge': 1.3.14 node-forge: 1.4.0 + semver-compare@1.0.0: + optional: true + semver-regex@4.0.5: {} semver@7.7.4: {} @@ -31105,6 +31457,11 @@ snapshots: serialize-error@2.1.0: {} + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + serialize-javascript@7.0.5: {} seroval-plugins@1.5.0(seroval@1.5.2): @@ -31542,6 +31899,9 @@ snapshots: sprintf-js@1.0.3: {} + sprintf-js@1.1.3: + optional: true + srvx@0.10.1: {} srvx@0.11.15: {} @@ -31830,6 +32190,12 @@ snapshots: dependencies: s.color: 0.0.15 + sumchecker@3.0.1: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + superagent@8.1.2: dependencies: component-emitter: 1.3.1 @@ -32229,6 +32595,9 @@ snapshots: type-detect@4.1.0: {} + type-fest@0.13.1: + optional: true + type-fest@0.16.0: {} type-fest@0.21.3: {}