OmniRoute/open-sse/mcp-server/scopeEnforcement.ts
diegosouzapw 20860877b8 feat: add TypeScript types and modularize translator registry
- Add type annotations to all providerModels helper functions
- Introduce LegacyProvider interface in providerRegistry
- Refactor translator system to use self-registering module pattern
  with bootstrapTranslatorRegistry() and per-file imports
- Simplify translator/index.ts by delegating to modular translators
- Remove hardcoded Gemini OAuth client secret for security
2026-03-04 18:46:49 -03:00

133 lines
3.5 KiB
TypeScript

import { MCP_TOOL_MAP } from "./schemas/tools.ts";
type AuthInfoLike = {
clientId?: string;
scopes?: string[];
};
export type McpToolExtraLike = {
authInfo?: AuthInfoLike;
sessionId?: string;
_meta?: unknown;
};
export type ScopeSource = "authInfo" | "meta" | "env" | "none";
export interface CallerScopeContext {
callerId: string;
scopes: string[];
source: ScopeSource;
}
export interface ScopeCheckResult {
allowed: boolean;
required: string[];
provided: string[];
missing: string[];
reason?: string;
}
function normalizeScopeList(raw: unknown): string[] {
if (!Array.isArray(raw)) return [];
const normalized = raw
.filter((value) => typeof value === "string")
.map((value) => value.trim())
.filter(Boolean);
return Array.from(new Set(normalized));
}
function extractMetaScopeList(meta: unknown): string[] {
if (!meta || typeof meta !== "object") return [];
const metaRecord = meta as Record<string, unknown>;
const direct = normalizeScopeList(metaRecord.scopes);
if (direct.length > 0) return direct;
const auth = metaRecord.auth;
if (auth && typeof auth === "object") {
const authScopes = normalizeScopeList((auth as Record<string, unknown>).scopes);
if (authScopes.length > 0) return authScopes;
}
const omni = metaRecord.omniroute;
if (omni && typeof omni === "object") {
const omniScopes = normalizeScopeList((omni as Record<string, unknown>).scopes);
if (omniScopes.length > 0) return omniScopes;
}
return [];
}
function scopeMatches(grantedScope: string, requiredScope: string): boolean {
if (grantedScope === "*" || grantedScope === requiredScope) {
return true;
}
if (grantedScope.endsWith("*")) {
const prefix = grantedScope.slice(0, -1);
return requiredScope.startsWith(prefix);
}
return false;
}
export function resolveCallerScopeContext(
extra: McpToolExtraLike | undefined,
fallbackScopes: readonly string[] = []
): CallerScopeContext {
const callerId =
(typeof extra?.authInfo?.clientId === "string" && extra.authInfo.clientId.trim()) ||
(typeof extra?.sessionId === "string" && extra.sessionId.trim()) ||
"anonymous";
const authScopes = normalizeScopeList(extra?.authInfo?.scopes);
if (authScopes.length > 0) {
return { callerId, scopes: authScopes, source: "authInfo" };
}
const metaScopes = extractMetaScopeList(extra?._meta);
if (metaScopes.length > 0) {
return { callerId, scopes: metaScopes, source: "meta" };
}
const fallback = normalizeScopeList(fallbackScopes);
if (fallback.length > 0) {
return { callerId, scopes: fallback, source: "env" };
}
return { callerId, scopes: [], source: "none" };
}
export function evaluateToolScopes(
toolName: string,
callerScopes: readonly string[],
enforceScopes: boolean
): ScopeCheckResult {
const toolDef = MCP_TOOL_MAP[toolName];
if (!toolDef) {
return {
allowed: false,
required: [],
provided: Array.from(callerScopes),
missing: [],
reason: "tool_definition_missing",
};
}
const required = Array.isArray(toolDef.scopes) ? Array.from(toolDef.scopes) : [];
const provided = normalizeScopeList(callerScopes);
if (!enforceScopes || required.length === 0) {
return { allowed: true, required, provided, missing: [] };
}
const missing = required.filter(
(requiredScope) => !provided.some((grantedScope) => scopeMatches(grantedScope, requiredScope))
);
return {
allowed: missing.length === 0,
required,
provided,
missing,
reason: missing.length > 0 ? "missing_scopes" : undefined,
};
}