| Location | Name | Required | Meaning |
|---|---|---|---|
| query | q | Yes | Required filtered search; empty or too short is rejected. |
| query | by | Conditional / optional | {"type":"string","enum":["email","username","name","sub"],"default":"email"} |
| query | cursor | Conditional / optional | {"type":"string"} |
{error:{code,message}} for CloudError failures, attaches Request-Id, and disables caching. Handle 401 by signing in; show role refusal for 403 forbidden, reauthenticate only for step_up_required, and collect a reason for 400 reason_required. A CSRF failure needs a current page/session token, not blind retries. Operation-specific 404/409/413/422/502/503 errors retain their status. Honor Retry-After on 429./**
* GET /api/admin/accounts?q=&by=email|username|name|sub&cursor= (accounts.search, ADMIN.md §8): one filtered Cognito
* ListUsers page (≤ 60) with each row's deletion hold. Rows carry the masked email only; the full email and name are
* on the detail endpoint. Never an unfiltered listing: an empty or too-short query is a 400.
*/
export const GET = adminRoute("accounts.search", async (request) => {
const params = new URL(request.url).searchParams;
const by = (params.get("by") ?? "email") as SearchBy;
if (!SEARCH_BY.includes(by)) throw invalid("by must be email, username, name or sub.");
const page = await searchUsers({ q: params.get("q") ?? "", by, cursor: params.get("cursor") || undefined });
return adminJson({ items: await withHolds(page.items), cursor: page.cursor ?? null });
}, { rateLimit: "accounts.search" });