| Location | Name | Required | Meaning |
|---|---|---|---|
| path | id | Yes | Use the ID from an authorized staff response; source validators reject malformed IDs. |
| header | X-Admin-CSRF | Yes | Session-bound token from the staff page; not a developer credential. |
{
"type": "object",
"properties": {
"text": {
"type": "string",
"minLength": 1,
"maxLength": 20000
},
"html": {
"type": "string",
"maxLength": 200000
},
"status": {
"type": "string",
"enum": [
"open",
"pending",
"solved",
"spam"
]
},
"attachments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"type": {
"type": "string",
"maxLength": 100
},
"data": {
"type": "string",
"format": "byte",
"description": "Base64; decoded attachment max 5 MiB. Entire Lambda request must fit 6 MiB."
}
},
"required": [
"name",
"type",
"data"
]
},
"maxItems": 10
}
},
"required": [
"text"
]
}{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/support/tickets/:id/reply (support.reply): mails the reply through SES (503 mail_unavailable until set up). */
export const POST = adminRoute("support.reply", async (request, ctx, { params }: Context) => {
const id = ticketIdParam((await params).id);
if (Number(request.headers.get("content-length") ?? 0) > MAX_REQUEST) throw new CloudError(413, "too_large", "Attachments are too large for one reply (about 4 MB in total). Send them in separate replies.");
const body = await readBody(request, BODY);
const result = await sendReply(ctx, id, {
text: body.text, html: body.html, status: body.status,
attachments: body.attachments?.map((file) => ({ name: file.name, type: file.type, data: new Uint8Array(Buffer.from(file.data, "base64")) })),
});
return adminJson(result, { status: 201 });
}, { rateLimit: "support-reply" });