import type { Repository } from '../../db/repository.js'; import { resolveOrgIds } from '../auth.js'; export interface A2aDelegation { /** Primary key of the a2a_delegations row. */ id: string; userId: string; clientId: string; grantId: string; grantedSpaceIds: string[]; grantedSkills: string[]; expiresAt: string | null; revokedAt: string | null; } /** 委任が有効か(失効していない & 期限内)。nowIso は呼び出し側が渡す(テスト決定論のため)。 */ export function isDelegationLive( d: { expiresAt: string | null; revokedAt: string | null }, nowIso: string, ): boolean { if (d.revokedAt) return false; if (d.expiresAt && new Date(d.expiresAt).getTime() <= new Date(nowIso).getTime()) return false; return true; } /** * computeEffectiveScope に渡せる最小限の principal 型。 * A2aPrincipal (token-auth.ts) はこのインターフェースを満たす。 * 循環 import を避けるためここで独立定義。 */ export interface A2aPrincipalLike { actingUserId: string; /** OAuth クライアント識別子。audit log 記録用(省略可)。 */ clientId?: string; delegation: { grantedSpaceIds: string[]; grantedSkills: string[]; }; } export interface EffectiveScope { viewer: Express.User; userVisibleSpaceIds: string[]; allowlistsBySpace: Record; effectiveSpaceIds: string[]; effectiveSkills: string[]; } /** * acting user の実際の可視スペース・スキルを delegation の AND 交差で絞り込む。 * user が存在しないまたは active でない場合は null を返す(fail-closed)。 */ export async function computeEffectiveScope( repo: Repository, principal: A2aPrincipalLike, ): Promise { const u = repo.getUserById(principal.actingUserId); if (!u || u.status !== 'active') return null; const viewer = { id: u.id, role: u.role, orgIds: resolveOrgIds(repo, u.id), status: u.status, name: u.name, } as Express.User; const visible = await repo.listSpaces({ viewer }); const userVisibleSpaceIds = visible.map(s => s.id); const allowlistsBySpace: Record = {}; for (const s of visible) { allowlistsBySpace[s.id] = repo.getSpaceA2aSkills(s.id); } const effectiveSpaceIds = resolveEffectiveSpaces( principal.delegation.grantedSpaceIds, userVisibleSpaceIds, ); const effectiveSkills = resolveEffectiveSkills( principal.delegation.grantedSkills, allowlistsBySpace, effectiveSpaceIds, ); return { viewer, userVisibleSpaceIds, allowlistsBySpace, effectiveSpaceIds, effectiveSkills }; } /** AND 交差: 委任が許可したスペース ∩ acting user が実際に見えるスペース。 */ export function resolveEffectiveSpaces(grantedSpaceIds: string[], userVisibleSpaceIds: string[]): string[] { const visible = new Set(userVisibleSpaceIds); return [...new Set(grantedSpaceIds)].filter(id => visible.has(id)); } /** AND 交差: 委任スキル ∩ (有効スペースの A2A 許可リストの和集合)。重複排除。 */ export function resolveEffectiveSkills( grantedSkills: string[], spaceAllowlistsBySpace: Record, effectiveSpaceIds: string[], ): string[] { const allowed = new Set(); for (const sid of effectiveSpaceIds) { for (const skill of (spaceAllowlistsBySpace[sid] ?? [])) allowed.add(skill); } const grantedSet = new Set(grantedSkills); return [...allowed].filter(s => grantedSet.has(s)); }