126 lines
4.2 KiB
TypeScript
126 lines
4.2 KiB
TypeScript
import { verifyCsrf, type Context } from "@wrnexus/core";
|
|
import { createAuthHttpHandlers } from "../http/index.ts";
|
|
import {
|
|
getDefaultAuthEngine,
|
|
getDefaultAuthRouteOptions,
|
|
getDefaultAuthSchemas,
|
|
hasDefaultAuthEngine,
|
|
} from "../runtime.ts";
|
|
|
|
export type AuthHttpHandlers = ReturnType<typeof createAuthHttpHandlers>;
|
|
import { AUTH_ROUTE_DEFINITIONS, type AuthHandlerName } from "./definitions.ts";
|
|
|
|
const ROUTES = Object.fromEntries(
|
|
AUTH_ROUTE_DEFINITIONS.map((definition) => [definition.path, definition]),
|
|
) as Readonly<Record<string, (typeof AUTH_ROUTE_DEFINITIONS)[number]>>;
|
|
|
|
function unavailable(): Response {
|
|
return Response.json(
|
|
{
|
|
ok: false,
|
|
error: "WRN-AUTH-NOT-CONFIGURED",
|
|
message: "Configure auth.engine before serving package auth routes.",
|
|
},
|
|
{ status: 503, headers: { "cache-control": "no-store" } },
|
|
);
|
|
}
|
|
|
|
function normalizeRoutePath(value: string): string {
|
|
try {
|
|
return (decodeURIComponent(value).replace(/\/+$/, "") || "/").toLowerCase();
|
|
} catch {
|
|
return (value.replace(/\/+$/, "") || "/").toLowerCase();
|
|
}
|
|
}
|
|
|
|
function requestRoutePath(ctx: Context): string {
|
|
const matchedRoute = ctx.locals.__wrnexusRoute;
|
|
if (typeof matchedRoute === "string" && matchedRoute.startsWith("/api/auth/")) {
|
|
return normalizeRoutePath(matchedRoute);
|
|
}
|
|
try {
|
|
return normalizeRoutePath(new URL(ctx.req.url).pathname);
|
|
} catch {
|
|
return normalizeRoutePath(ctx.url.pathname);
|
|
}
|
|
}
|
|
|
|
function methodNotAllowed(allowed: readonly string[]): Response {
|
|
return Response.json(
|
|
{ ok: false, error: "Method Not Allowed" },
|
|
{ status: 405, headers: { allow: allowed.join(", "), "cache-control": "no-store" } },
|
|
);
|
|
}
|
|
|
|
function handlersFor(ctx: Context): AuthHttpHandlers | undefined {
|
|
if (!hasDefaultAuthEngine()) return undefined;
|
|
const routeOptions = getDefaultAuthRouteOptions();
|
|
return createAuthHttpHandlers({
|
|
engine: getDefaultAuthEngine(),
|
|
schemas: getDefaultAuthSchemas(),
|
|
baseUrl: routeOptions.baseUrl ?? ctx.url.origin,
|
|
passkey: routeOptions.passkey,
|
|
onSignedIn: routeOptions.onSignedIn,
|
|
onSignedOut: routeOptions.onSignedOut,
|
|
});
|
|
}
|
|
|
|
/** Invoke one concrete handler. Route-specific entry modules use this path. */
|
|
export async function invokeAuthHandler(name: AuthHandlerName, ctx: Context): Promise<Response> {
|
|
const handlers = handlersFor(ctx);
|
|
if (!handlers) return unavailable();
|
|
|
|
const method = ctx.req.method.toUpperCase();
|
|
const unsafeMethod = method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
|
|
if (unsafeMethod && getDefaultAuthRouteOptions().csrf !== false && !verifyCsrf(ctx)) {
|
|
return Response.json(
|
|
{ ok: false, error: "Invalid CSRF token" },
|
|
{ status: 403, headers: { "cache-control": "no-store" } },
|
|
);
|
|
}
|
|
|
|
try {
|
|
return await handlers[name](ctx);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
if (message.startsWith("WRN-AUTH-PASSKEY-PROVIDER:")) {
|
|
return Response.json(
|
|
{
|
|
ok: false,
|
|
error: "Passkeys are unavailable",
|
|
code: "passkey-provider-not-configured",
|
|
},
|
|
{ status: 503, headers: { "cache-control": "no-store" } },
|
|
);
|
|
}
|
|
console.error(`[wrnexus:auth] ${name} failed`, error);
|
|
return Response.json(
|
|
{ ok: false, error: "Authentication request failed" },
|
|
{ status: 500, headers: { "cache-control": "no-store" } },
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Backward-compatible dispatcher for applications importing the shared route. */
|
|
export async function dispatchAuthRoute(routePath: string, ctx: Context): Promise<Response> {
|
|
const definition = ROUTES[normalizeRoutePath(routePath)];
|
|
if (!definition) {
|
|
return Response.json(
|
|
{ ok: false, error: "Not Found" },
|
|
{ status: 404, headers: { "cache-control": "no-store" } },
|
|
);
|
|
}
|
|
const method = ctx.req.method.toUpperCase();
|
|
if (!definition.methods.some((allowed) => allowed === method)) {
|
|
return methodNotAllowed(definition.methods);
|
|
}
|
|
return invokeAuthHandler(definition.handler, ctx);
|
|
}
|
|
|
|
export default async function authApi(ctx: Context): Promise<Response> {
|
|
return dispatchAuthRoute(requestRoutePath(ctx), ctx);
|
|
}
|
|
|
|
export const GET = authApi;
|
|
export const POST = authApi;
|