From aa4ce99419d6704ca83ed2c549dbcfd686b3904e Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Thu, 26 Feb 2026 14:48:14 -0800 Subject: [PATCH] Add "Invalidate OAuth token" developer menu item --- apps/twig/src/main/menu.ts | 6 +++++ apps/twig/src/main/services/ui/schemas.ts | 2 ++ apps/twig/src/main/services/ui/service.ts | 4 +++ apps/twig/src/main/trpc/routers/ui.ts | 1 + .../components/GlobalEventHandlers.tsx | 27 +++++++++++++++++++ 5 files changed, 40 insertions(+) diff --git a/apps/twig/src/main/menu.ts b/apps/twig/src/main/menu.ts index a424d7e5e..23ab31531 100644 --- a/apps/twig/src/main/menu.ts +++ b/apps/twig/src/main/menu.ts @@ -169,6 +169,12 @@ function buildFileMenu(): MenuItemConstructorOptions { container.get(MAIN_TOKENS.UIService).clearStorage(); }, }, + { + label: "Invalidate OAuth token", + click: () => { + container.get(MAIN_TOKENS.UIService).invalidateToken(); + }, + }, { label: "Mark all agent sessions for recreation", click: () => { diff --git a/apps/twig/src/main/services/ui/schemas.ts b/apps/twig/src/main/services/ui/schemas.ts index e46b210fc..ce77eecb9 100644 --- a/apps/twig/src/main/services/ui/schemas.ts +++ b/apps/twig/src/main/services/ui/schemas.ts @@ -4,6 +4,7 @@ export const UIServiceEvent = { NewTask: "new-task", ResetLayout: "reset-layout", ClearStorage: "clear-storage", + InvalidateToken: "invalidate-token", } as const; // UI events are simple signals - payload is just a marker that the event fired @@ -12,4 +13,5 @@ export interface UIServiceEvents { [UIServiceEvent.NewTask]: true; [UIServiceEvent.ResetLayout]: true; [UIServiceEvent.ClearStorage]: true; + [UIServiceEvent.InvalidateToken]: true; } diff --git a/apps/twig/src/main/services/ui/service.ts b/apps/twig/src/main/services/ui/service.ts index 9b3bd3ec5..2abc11a9b 100644 --- a/apps/twig/src/main/services/ui/service.ts +++ b/apps/twig/src/main/services/ui/service.ts @@ -19,4 +19,8 @@ export class UIService extends TypedEventEmitter { clearStorage(): void { this.emit(UIServiceEvent.ClearStorage, true); } + + invalidateToken(): void { + this.emit(UIServiceEvent.InvalidateToken, true); + } } diff --git a/apps/twig/src/main/trpc/routers/ui.ts b/apps/twig/src/main/trpc/routers/ui.ts index 20419247e..b971a371c 100644 --- a/apps/twig/src/main/trpc/routers/ui.ts +++ b/apps/twig/src/main/trpc/routers/ui.ts @@ -24,4 +24,5 @@ export const uiRouter = router({ onNewTask: subscribeToUIEvent(UIServiceEvent.NewTask), onResetLayout: subscribeToUIEvent(UIServiceEvent.ResetLayout), onClearStorage: subscribeToUIEvent(UIServiceEvent.ClearStorage), + onInvalidateToken: subscribeToUIEvent(UIServiceEvent.InvalidateToken), }); diff --git a/apps/twig/src/renderer/components/GlobalEventHandlers.tsx b/apps/twig/src/renderer/components/GlobalEventHandlers.tsx index d94386af9..93bdc8233 100644 --- a/apps/twig/src/renderer/components/GlobalEventHandlers.tsx +++ b/apps/twig/src/renderer/components/GlobalEventHandlers.tsx @@ -1,3 +1,4 @@ +import { useAuthStore } from "@features/auth/stores/authStore"; import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore"; import { useRightSidebarStore } from "@features/right-sidebar"; import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore"; @@ -9,8 +10,10 @@ import { useFocusWorkspace } from "@features/workspace/hooks/useFocusWorkspace"; import { useWorkspaceStore } from "@features/workspace/stores/workspaceStore"; import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts"; import { clearApplicationStorage } from "@renderer/lib/clearStorage"; +import { logger } from "@renderer/lib/logger"; import { useRegisteredFoldersStore } from "@renderer/stores/registeredFoldersStore"; import { trpcReact } from "@renderer/trpc"; +import { trpcVanilla } from "@renderer/trpc/client"; import type { Task } from "@shared/types"; import { useNavigationStore } from "@stores/navigationStore"; import { useCallback, useEffect, useMemo } from "react"; @@ -139,6 +142,26 @@ export function GlobalEventHandlers({ clearApplicationStorage(); }, []); + const handleInvalidateToken = useCallback((data?: unknown) => { + if (!data) return; + const log = logger.scope("global-event-handlers"); + const state = useAuthStore.getState(); + const currentToken = state.oauthAccessToken; + if (!currentToken) { + log.warn("No access token to invalidate"); + return; + } + const invalidToken = `${currentToken}_invalid`; + useAuthStore.setState({ oauthAccessToken: invalidToken }); + trpcVanilla.agent.updateToken + .mutate({ token: invalidToken }) + .catch((err) => log.warn("Failed to update agent token", err)); + trpcVanilla.cloudTask.updateToken + .mutate({ token: invalidToken }) + .catch((err) => log.warn("Failed to update cloud task token", err)); + log.info("OAuth access token invalidated for testing"); + }, []); + const globalOptions = { enableOnFormTags: true, enableOnContentEditable: true, @@ -244,5 +267,9 @@ export function GlobalEventHandlers({ onData: handleClearStorage, }); + trpcReact.ui.onInvalidateToken.useSubscription(undefined, { + onData: handleInvalidateToken, + }); + return null; }