| Location | Name | Required | Meaning |
|---|---|---|---|
| header | X-Admin-CSRF | Yes | Session-bound token from the staff page; not a developer credential. |
{
"oneOf": [
{
"type": "object",
"properties": {
"mode": {
"const": "invite"
},
"email": {
"type": "string",
"format": "email",
"maxLength": 254
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"role": {
"type": "string",
"enum": [
"owner",
"support",
"billing",
"readonly"
]
},
"linkedCustomerSub": {
"type": "string",
"pattern": "^[\\w-]{1,64}$"
}
},
"required": [
"mode",
"email",
"name",
"role"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"mode": {
"const": "promote"
},
"email": {
"type": "string",
"format": "email",
"maxLength": 254
},
"role": {
"type": "string",
"enum": [
"owner",
"support",
"billing",
"readonly"
]
},
"name": {
"type": "string",
"maxLength": 100
}
},
"required": [
"mode",
"email",
"role"
],
"additionalProperties": false
}
]
}{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./**
* POST /api/admin/admins (admins.create, step-up):
* {mode:"invite", email, name, role, linkedCustomerSub?} new staff login; Cognito emails a temporary password
* {mode:"promote", email, role, name?} link exactly one customer with that verified email
* 201 {admin, emailed}. Audited in admins.ts (admin.invite / admin.promote).
*/
export const POST = adminRoute("admins.create", async (request, ctx) => {
const input = await readBody(request, body);
const result = input.mode === "invite" ? await inviteAdmin(ctx, input) : await promoteCustomer(ctx, input);
return adminJson(result, { status: 201 });
}, { rateLimit: "admins.create" });