Files
Termix/plugins/ai/backend/gating.ts
T
LukeGus 504710b065 feat: ship the AI assistant as a first-party in-process plugin
Moves src/backend/ai and its routes into plugins/ai, mounted through a
small /ai dispatcher instead of its own port. Moves the ai-enabled and
ai-private-endpoints settings routes out of user-settings-routes.ts
into the plugin too, so it has no leftover core dependents.
2026-09-21 15:20:05 -05:00

71 lines
2.1 KiB
TypeScript

import type { NextFunction, Response } from "express";
import type { AuthenticatedRequest } from "../../../src/types/index.js";
import {
createCurrentSettingsRepository,
createCurrentUserPreferenceRepository,
} from "../../../src/backend/database/repositories/factory.js";
/**
* Three gates, all checked on the server.
*
* The admin global is a hard kill switch: when it is off the feature does not
* exist for anyone, regardless of what any user has enabled. It defaults to
* false so upgrading an existing install turns nothing on by surprise.
*
* Mirrors the shape of isSharingEnabledForHost in the session-sharing routes,
* where the global also wins over the per-entity setting.
*/
export const AI_GLOBAL_ENABLED_KEY = "ai_globally_enabled";
export async function isAiGloballyEnabled(): Promise<boolean> {
return createCurrentSettingsRepository().getBoolean(
AI_GLOBAL_ENABLED_KEY,
false,
);
}
export interface AiAccess {
enabled: boolean;
allowReadOnlyCommands: boolean;
}
export async function resolveAiAccess(userId: string): Promise<AiAccess> {
const globalEnabled = await isAiGloballyEnabled();
if (!globalEnabled) {
return { enabled: false, allowReadOnlyCommands: false };
}
const preferences =
await createCurrentUserPreferenceRepository().findByUserId(userId);
return {
// Null means the user was never asked, which is not consent.
enabled: preferences?.aiAssistantEnabled === true,
allowReadOnlyCommands: preferences?.aiReadOnlyCommands === true,
};
}
/** Rejects any AI request unless both gates are open. */
export function createAiGate() {
return async (
req: AuthenticatedRequest,
res: Response,
next: NextFunction,
): Promise<void> => {
if (!req.userId) {
res.status(401).json({ error: "Authentication required" });
return;
}
const access = await resolveAiAccess(req.userId);
if (!access.enabled) {
res.status(403).json({ error: "The AI assistant is not enabled" });
return;
}
(req as AuthenticatedRequest & { aiAccess?: AiAccess }).aiAccess = access;
next();
};
}