fix(api): count Cognito pool users directly

The runtime leaves EstimatedNumberOfUsers at 0, so the pool metadata now
carries a userCount from ListUsers (capped at 1000), which the delete
confirmation reads.
This commit is contained in:
Zakaria
2026-10-23 17:54:15 -04:00
parent b4c2cadf5c
commit 43f4cae9b5
3 changed files with 34 additions and 4 deletions
@@ -57,6 +57,8 @@ import type {
/** Cognito caps every list page at 60. */
const COGNITO_PAGE_LIMIT = 60
/** Counting stops here so a very large pool cannot stall the pool list. */
const USER_COUNT_CAP = 1000
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
/**
@@ -72,6 +74,8 @@ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
* symbol, matching AWS.
* - `InitiateAuth` with `USER_PASSWORD_AUTH` issues real tokens once a user's
* password is permanent, so users created here can sign in immediately.
* - `EstimatedNumberOfUsers` stays 0 however many users exist (AWS also only
* updates it periodically), so `userCount` is counted with `ListUsers`.
* - An admin action the user's state does not allow (confirming a confirmed
* user) is `NotAuthorizedException`, surfaced here as a conflict because the
* console caller is authorised and a 401 would end the console session.
@@ -110,11 +114,12 @@ export class AwsUserPoolsAdapter implements CloudServiceAdapter {
async get(id: string): Promise<CloudResource | null> {
try {
const [pool, clients] = await Promise.all([
const [pool, clients, userCount] = await Promise.all([
this.cognito.send(new DescribeUserPoolCommand({UserPoolId: id})),
this.listClients(id),
this.countUsers(id),
])
return pool.UserPool ? toResource(pool.UserPool, clients) : null
return pool.UserPool ? toResource(pool.UserPool, clients, userCount) : null
} catch (error) {
if (isMissing(error)) return null
throw error
@@ -284,6 +289,17 @@ export class AwsUserPoolsAdapter implements CloudServiceAdapter {
return listGroupNamesForUser(this.cognito, poolId, username)
}
private async countUsers(poolId: string): Promise<number> {
let count = 0
let token: string | undefined
do {
const res = await this.cognito.send(new ListUsersCommand({UserPoolId: poolId, Limit: COGNITO_PAGE_LIMIT, ...(token ? {PaginationToken: token} : {})}))
count += res.Users?.length ?? 0
token = res.PaginationToken || undefined
} while (token && count < USER_COUNT_CAP)
return Math.min(count, USER_COUNT_CAP)
}
private async listClients(poolId: string): Promise<UserPoolClientDescription[]> {
const clients: UserPoolClientDescription[] = []
let nextToken: string | undefined
@@ -423,7 +439,7 @@ function toSummaryResource(pool: UserPoolDescriptionType): CloudResource[] {
}]
}
function toResource(pool: UserPoolType, clients: UserPoolClientDescription[]): CloudResource {
function toResource(pool: UserPoolType, clients: UserPoolClientDescription[], userCount: number): CloudResource {
const id = pool.Id ?? ''
const policy = pool.Policies?.PasswordPolicy
return {
@@ -440,6 +456,7 @@ function toResource(pool: UserPoolType, clients: UserPoolClientDescription[]): C
signInWith: pool.UsernameAttributes?.includes('email') ? 'email' : 'username',
passwordPolicy: policy ? describePasswordPolicy(policy) : null,
estimatedNumberOfUsers: pool.EstimatedNumberOfUsers ?? null,
userCount,
mfaConfiguration: pool.MfaConfiguration ?? null,
deletionProtection: pool.DeletionProtection ?? null,
appClients: clients.map((client) => ({clientId: client.ClientId ?? '', clientName: client.ClientName ?? ''})),