feat: expose core identity transport
CI / verify (pull_request) Successful in 27s
CI / infrastructure-smoke (pull_request) Successful in 19s
CI / migration-smoke (pull_request) Successful in 26s

This commit is contained in:
ekklesia
2026-07-21 19:15:14 -05:00
parent 82fe338a8d
commit 91d8f5946f
33 changed files with 2672 additions and 224 deletions
+72
View File
@@ -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;
}