/** * Reads the hard-revocation deny-list the pre-token Lambda consults at mint time. * * The group-sync path (Google → Cognito groups, 5-min cadence) is the PRIMARY * entitlement control; this deny-list is a fast-kill OVERLAY for incident response * ("revoke this compromised `sub` now") that does not wait for the next sync. An * entry keyed by the user's `sub` means: suppress every tier scope on this mint, * dropping the principal to no MCP access until the (TTL'd) entry expires. * * Failure posture is deliberately fail-OPEN: a deny-list read error resolves to * "not denied" (logged loudly for a metric-filter alarm) rather than denying, so * a DynamoDB blip cannot lock every principal out of token issuance. The primary * group-membership control — and its own fail-CLOSED 30-min freshness window — * still governs entitlement. (design.md §2.3; security-review C2.) */ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb'; let cached: DynamoDBDocumentClient | undefined; function doc(): DynamoDBDocumentClient { cached ??= DynamoDBDocumentClient.from(new DynamoDBClient({})); return cached; } /** * True iff `sub` has an active deny-list entry. A read failure resolves to * `false` (fail OPEN — see module note) after a structured warn log so the * outage is observable. An empty/absent `sub` is never denied. */ export async function isSubDenied( tableName: string, sub: string | undefined, client: DynamoDBDocumentClient = doc(), ): Promise { if (!sub) return false; try { const res = await client.send(new GetCommand({ TableName: tableName, Key: { sub } })); return res.Item !== undefined; } catch (err) { console.warn( JSON.stringify({ event: 'deny_list_read_failed', reason: err instanceof Error ? err.message : 'unknown', failedOpen: true, }), ); return false; } }