/** * The Google Group → Cognito group mapping this sync reconciles, and the pure * membership-diff logic. Google Groups are the single source of truth for * entitlement (design.md §2.3); this Lambda mirrors their membership into the * matching Cognito groups every 5 minutes so the pre-token Lambda can map * Cognito group → suppressed scopes without a hot-path Directory call. */ /** Workspace domain the managed groups live under. Overridable via env at deploy. */ export const DEFAULT_DOMAIN = 'seahavenind.com'; /** The four managed groups (Cognito group name === Google Group local-part). */ export const MANAGED_GROUP_NAMES = [ 'sh-mcp-ops', 'sh-mcp-assistant', 'sh-mcp-finance', 'sh-mcp-admin', ] as const; export type ManagedGroupName = (typeof MANAGED_GROUP_NAMES)[number]; export interface ManagedGroup { /** Cognito group name. */ cognito: ManagedGroupName; /** Fully-qualified Google Group address. */ googleEmail: string; } /** Build the managed-group list for a workspace domain. */ export function managedGroups(domain: string = DEFAULT_DOMAIN): ManagedGroup[] { return MANAGED_GROUP_NAMES.map((cognito) => ({ cognito, googleEmail: `${cognito}@${domain}` })); } /** Normalize an email for set comparison (Google/Cognito casing is not stable). */ export function normalizeEmail(email: string): string { return email.trim().toLowerCase(); } export interface MembershipDiff { toAdd: string[]; toRemove: string[]; } /** * Compute the membership changes to make a Cognito group match the Google group. * Comparison is case-insensitive; returned values are normalized (lowercased). * * @param current members currently in the Cognito group. * @param desired members the Google group says should be present. */ export function computeMembershipDiff( current: readonly string[], desired: readonly string[], ): MembershipDiff { const cur = new Set(current.map(normalizeEmail)); const des = new Set(desired.map(normalizeEmail)); return { toAdd: [...des].filter((e) => !cur.has(e)), toRemove: [...cur].filter((e) => !des.has(e)), }; }