103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
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<string, string[]>;
|
||
effectiveSpaceIds: string[];
|
||
effectiveSkills: string[];
|
||
}
|
||
|
||
/**
|
||
* acting user の実際の可視スペース・スキルを delegation の AND 交差で絞り込む。
|
||
* user が存在しないまたは active でない場合は null を返す(fail-closed)。
|
||
*/
|
||
export async function computeEffectiveScope(
|
||
repo: Repository,
|
||
principal: A2aPrincipalLike,
|
||
): Promise<EffectiveScope | null> {
|
||
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<string, string[]> = {};
|
||
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<string, string[]>,
|
||
effectiveSpaceIds: string[],
|
||
): string[] {
|
||
const allowed = new Set<string>();
|
||
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));
|
||
}
|