feat(api): port Fase 5 REST API — Bun.serve router + JWT middleware + auth/session/conversation/chat handlers + composition root

This commit is contained in:
asepharyana
2026-09-02 22:50:38 +07:00
parent 1b130e69b3
commit 49e79322b4
14 changed files with 1007 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
/**
* Typed API error with automatic HTTP response conversion.
* Mirrors `apps/interfaces/api/src/error.rs`.
*
* Each kind maps to a status code:
* - BadRequest → 400
* - Unauthorized → 401
* - NotFound → 404
* - Conflict → 409
* - TooManyRequests → 429
* - Internal → 500
* - ChatProxy → 502
*/
export type ApiErrorKind =
| "BadRequest"
| "Unauthorized"
| "NotFound"
| "Conflict"
| "TooManyRequests"
| "Internal"
| "ChatProxy";
export const HTTP_STATUS: Record<ApiErrorKind, number> = {
BadRequest: 400,
Unauthorized: 401,
NotFound: 404,
Conflict: 409,
TooManyRequests: 429,
Internal: 500,
ChatProxy: 502,
};
export class ApiError extends Error {
constructor(public kind: ApiErrorKind, message: string) {
super(message);
this.name = "ApiError";
}
get status(): number {
return HTTP_STATUS[this.kind];
}
static badRequest(msg: string): ApiError {
return new ApiError("BadRequest", msg);
}
static unauthorized(msg: string): ApiError {
return new ApiError("Unauthorized", msg);
}
static notFound(msg: string): ApiError {
return new ApiError("NotFound", msg);
}
static conflict(msg: string): ApiError {
return new ApiError("Conflict", msg);
}
static tooManyRequests(msg: string): ApiError {
return new ApiError("TooManyRequests", msg);
}
static internal(msg: string): ApiError {
return new ApiError("Internal", msg);
}
static chatProxy(msg: string): ApiError {
return new ApiError("ChatProxy", msg);
}
/** Whether an internal error's details should be hidden from the client. */
get exposeDetails(): boolean {
// Internal errors hide details; all others surface the message.
return this.kind !== "Internal";
}
}