/** * Group-sync Lambda entrypoint — runs every 5 minutes (EventBridge). * * Mirrors Google Group membership into the matching Cognito groups and, on full * success, writes the freshness marker the pre-token Lambda fails closed on. Any * error propagates so the CloudWatch alarm fires and the marker stays stale. * * Reads the Google service-account credentials from Secrets Manager on cold * start (never an env var — secrets-and-config handbook rule). */ import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'; import { GoogleDirectoryReader, type GoogleServiceAccount } from './clients.js'; import { CognitoGroupSync, DynamoSyncStateWriter } from './aws.js'; import { runSync } from './sync.js'; import { DEFAULT_DOMAIN } from './groups.js'; let cachedSa: GoogleServiceAccount | undefined; async function loadServiceAccount(secretArn: string): Promise { if (cachedSa) return cachedSa; const sm = new SecretsManagerClient({}); const res = await sm.send(new GetSecretValueCommand({ SecretId: secretArn })); if (!res.SecretString) throw new Error('Google SA secret has no SecretString'); cachedSa = JSON.parse(res.SecretString) as GoogleServiceAccount; return cachedSa; } function requireEnv(name: string): string { const v = process.env[name]; if (!v) throw new Error(`Missing required env var ${name}`); return v; } export async function handler(): Promise<{ ok: true; results: unknown }> { const userPoolId = requireEnv('USER_POOL_ID'); const syncStateTable = requireEnv('SYNC_STATE_TABLE'); const saSecretArn = requireEnv('GOOGLE_SA_SECRET_ARN'); const domain = process.env['WORKSPACE_DOMAIN'] ?? DEFAULT_DOMAIN; const sa = await loadServiceAccount(saSecretArn); const results = await runSync({ directory: new GoogleDirectoryReader(sa), cognito: new CognitoGroupSync(userPoolId), syncState: new DynamoSyncStateWriter(syncStateTable), domain, }); console.log(JSON.stringify({ event: 'group_sync_ok', results })); return { ok: true, results }; }