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 <madison.w.kennedy@gmail.com>
This commit is contained in:
Devin AI 2026-07-08 01:56:44 +00:00
parent e26f326d8c
commit 2f58d3ecd2
4 changed files with 55 additions and 16 deletions

View File

@ -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;

View File

@ -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<string, InstalledAppConfig>;
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));
}

View File

@ -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);
});
});

View File

@ -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<string, InstalledAppConfig>;
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;
}