From 2f58d3ecd27a19407112d08a84d7eb0e29385398 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 01:56:44 +0000 Subject: [PATCH] fix(agent-auth): make agent-auth independently enableable via shared resolver The dashboard treated agent-auth as a sub-app whose enablement inherited from the authentication parent, while the backend and both enable paths keyed off agent-auth's own flag. The dashboard page therefore stayed 'App is not enabled' after enabling. Add a shared isAppEnabledInInstalledApps resolver (used by backend + dashboard) and mark agent-auth independentlyEnableable so it is governed by its own flag; other sub-apps (e.g. fraud-protection) keep parent-inheritance. Co-Authored-By: madison@stack-auth.com --- apps/backend/src/lib/agent-auth/requests.ts | 3 ++- apps/dashboard/src/lib/apps-utils.ts | 18 +++---------- packages/shared/src/apps/apps-config.test.ts | 28 ++++++++++++++++++++ packages/shared/src/apps/apps-config.ts | 22 +++++++++++++++ 4 files changed, 55 insertions(+), 16 deletions(-) create mode 100644 packages/shared/src/apps/apps-config.test.ts diff --git a/apps/backend/src/lib/agent-auth/requests.ts b/apps/backend/src/lib/agent-auth/requests.ts index 2bb856139..1c704d54f 100644 --- a/apps/backend/src/lib/agent-auth/requests.ts +++ b/apps/backend/src/lib/agent-auth/requests.ts @@ -1,6 +1,7 @@ import { getSoleTenancyFromProjectBranch } from "@/lib/tenancies"; import { AGENT_AUTH_APP_ID } from "./constants"; import { StatusError } from "@hexclave/shared/dist/utils/errors"; +import { isAppEnabledInInstalledApps } from "@hexclave/shared/dist/apps/apps-config"; import type { SmartRequest } from "@/route-handlers/smart-request"; export function getHeader(req: SmartRequest, name: string): string | null { @@ -26,7 +27,7 @@ export async function getAgentAuthTenancy(projectId: string) { if (!tenancy) { throw new StatusError(StatusError.NotFound, "Agent auth project not found"); } - if (!tenancy.config.apps.installed[AGENT_AUTH_APP_ID]?.enabled) { + if (!isAppEnabledInInstalledApps(tenancy.config.apps.installed, AGENT_AUTH_APP_ID)) { throw new StatusError(StatusError.NotFound, "Not found"); } return tenancy; diff --git a/apps/dashboard/src/lib/apps-utils.ts b/apps/dashboard/src/lib/apps-utils.ts index c98888d34..9ff68eb29 100644 --- a/apps/dashboard/src/lib/apps-utils.ts +++ b/apps/dashboard/src/lib/apps-utils.ts @@ -1,13 +1,7 @@ "use client"; import { ALL_APPS_FRONTEND, hasNavigationItems } from "@/lib/apps-frontend"; -import { ALL_APPS, getParentAppId, type AppId } from "@hexclave/shared/dist/apps/apps-config"; - -type InstalledAppConfig = { - enabled?: boolean, -} | undefined; - -export type InstalledAppsMap = Record; +import { ALL_APPS, isAppEnabledInInstalledApps, type AppId, type InstalledAppsMap } from "@hexclave/shared/dist/apps/apps-config"; /** * Get all available app IDs, filtering out alpha apps in production @@ -25,15 +19,10 @@ export function getAllAvailableAppIds(): AppId[] { /** * Determines whether an app is enabled. - * - Regular apps are enabled via their own config entry. - * - Sub-apps are enabled when their parent app is enabled. + * Uses the shared app registry enablement rules. */ export function isAppEnabled(installedApps: InstalledAppsMap, appId: AppId): boolean { - const parentAppId = getParentAppId(appId); - if (parentAppId != null) { - return installedApps[parentAppId]?.enabled ?? false; - } - return installedApps[appId]?.enabled ?? false; + return isAppEnabledInInstalledApps(installedApps, appId); } /** @@ -61,4 +50,3 @@ export function getUninstalledAppIds(installedApps: AppId[]): AppId[] { const installedSet = new Set(installedApps); return getAllAvailableAppIds().filter(appId => !installedSet.has(appId)); } - diff --git a/packages/shared/src/apps/apps-config.test.ts b/packages/shared/src/apps/apps-config.test.ts new file mode 100644 index 000000000..5ba1c133e --- /dev/null +++ b/packages/shared/src/apps/apps-config.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from "vitest"; +import { type InstalledAppsMap, isAppEnabledInInstalledApps } from "./apps-config"; + +describe("isAppEnabledInInstalledApps", () => { + test("independently-enableable apps use their own enabled flag", () => { + const installedApps: InstalledAppsMap = { + authentication: { enabled: true }, + "agent-auth": { enabled: false }, + }; + + expect(isAppEnabledInInstalledApps(installedApps, "agent-auth")).toBe(false); + + installedApps["agent-auth"] = { enabled: true }; + expect(isAppEnabledInInstalledApps(installedApps, "agent-auth")).toBe(true); + }); + + test("normal sub-apps still inherit from their parent", () => { + const installedApps: InstalledAppsMap = { + analytics: { enabled: true }, + "session-replays": { enabled: false }, + }; + + expect(isAppEnabledInInstalledApps(installedApps, "session-replays")).toBe(true); + + installedApps.analytics = { enabled: false }; + expect(isAppEnabledInInstalledApps(installedApps, "session-replays")).toBe(false); + }); +}); diff --git a/packages/shared/src/apps/apps-config.ts b/packages/shared/src/apps/apps-config.ts index 53e9bf643..25e524580 100644 --- a/packages/shared/src/apps/apps-config.ts +++ b/packages/shared/src/apps/apps-config.ts @@ -46,10 +46,17 @@ type App = { tags: (keyof typeof ALL_APP_TAGS)[], stage: "alpha" | "beta" | "stable", parentAppId?: ParentAppId, + independentlyEnableable?: boolean, }; export type AppId = keyof typeof ALL_APPS; +export type InstalledAppConfig = { + enabled?: boolean, +} | undefined; + +export type InstalledAppsMap = Record; + export const ALL_APPS = { "authentication": { displayName: "Authentication", @@ -63,6 +70,7 @@ export const ALL_APPS = { tags: ["auth", "security", "developers"], stage: "alpha", parentAppId: "authentication", + independentlyEnableable: true, }, "fraud-protection": { displayName: "Fraud Protection", @@ -199,3 +207,17 @@ export function getParentAppId(appId: AppId): AppId | null { const app = ALL_APPS[appId]; return "parentAppId" in app ? app.parentAppId : null; } + +export function isAppEnabledInInstalledApps(installedApps: InstalledAppsMap, appId: AppId): boolean { + const app = ALL_APPS[appId]; + if ("independentlyEnableable" in app) { + return installedApps[appId]?.enabled === true; + } + + const parentAppId = getParentAppId(appId); + if (parentAppId != null) { + return isAppEnabledInInstalledApps(installedApps, parentAppId); + } + + return installedApps[appId]?.enabled === true; +}