feat: expose core identity transport
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { isAuthenticatedPrincipal } from "@ekklesia/contracts";
|
||||
import { type EkklesiaIdentityRuntime, IdentityRuntimeError } from "@ekklesia/core/identity";
|
||||
import { Hono } from "hono";
|
||||
import type { Context } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
|
||||
export interface CreateApiAppOptions {
|
||||
readonly runtime: EkklesiaIdentityRuntime;
|
||||
/** Exact allowed browser origins. Never a wildcard; combined with `credentials: true`, only an
|
||||
* exact, reflected match is ever returned in `Access-Control-Allow-Origin`. */
|
||||
readonly trustedOrigins: readonly string[];
|
||||
}
|
||||
|
||||
function sanitizedErrorResponse(c: Context, error: unknown, status: 500 | 503) {
|
||||
const code = error instanceof IdentityRuntimeError ? error.code : "INTERNAL_ERROR";
|
||||
c.header("Cache-Control", "no-store");
|
||||
return c.json({ code }, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure Hono application factory: no I/O, no environment reads, no process side effects. Suitable
|
||||
* for unit and integration tests via `app.fetch` directly, with a fully injected identity runtime
|
||||
* standing in for real Better Auth/PostgreSQL where appropriate.
|
||||
*/
|
||||
export function createApiApp(options: CreateApiAppOptions): Hono {
|
||||
const { runtime } = options;
|
||||
const allowedOrigins = new Set(options.trustedOrigins);
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
const corsMiddleware = cors({
|
||||
allowHeaders: ["Content-Type"],
|
||||
allowMethods: ["GET", "POST", "OPTIONS"],
|
||||
credentials: true,
|
||||
maxAge: 600,
|
||||
origin: (origin) => (origin && allowedOrigins.has(origin) ? origin : undefined),
|
||||
});
|
||||
app.use("/api/auth/*", corsMiddleware);
|
||||
app.use("/api/v1/*", corsMiddleware);
|
||||
|
||||
app.on(["POST", "GET"], "/api/auth/*", async (c) => {
|
||||
try {
|
||||
return await runtime.handleAuthRequest(c.req.raw);
|
||||
} catch (error) {
|
||||
return sanitizedErrorResponse(c, error, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/v1/principal", async (c) => {
|
||||
c.header("Cache-Control", "no-store");
|
||||
c.header("Vary", "Origin, Cookie");
|
||||
try {
|
||||
const principal = await runtime.resolvePrincipal(c.req.raw.headers);
|
||||
return c.json(
|
||||
isAuthenticatedPrincipal(principal)
|
||||
? { kind: "authenticated" as const, userId: principal.userId }
|
||||
: { kind: "anonymous" as const },
|
||||
200,
|
||||
);
|
||||
} catch (error) {
|
||||
return sanitizedErrorResponse(c, error, 503);
|
||||
}
|
||||
});
|
||||
|
||||
app.notFound((c) => {
|
||||
c.header("Cache-Control", "no-store");
|
||||
return c.json({ code: "NOT_FOUND" }, 404);
|
||||
});
|
||||
app.onError((_error, c) => sanitizedErrorResponse(c, _error, 500));
|
||||
|
||||
return app;
|
||||
}
|
||||
Reference in New Issue
Block a user