release: WRNexusJS 0.5.0
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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;
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("startImpersonation", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("stopImpersonation", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("acceptInvitation", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("login", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("logout", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("requestMagicLink", ctx);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function GET(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("consumeMagicLink", ctx);
|
||||
}
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("consumeMagicLink", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("completeMfa", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("beginMfaOtp", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("completeOtpLogin", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("requestOtpLogin", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("verifyOtp", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("issueOtp", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("passkeyAuthenticationOptions", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("passkeyAuthenticationVerify", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("passkeyRegistrationOptions", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("passkeyRegistrationVerify", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("changePassword", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("requestPasswordReset", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("resetPassword", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("recoveryCodes", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("register", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("revokeSession", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function GET(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("sessions", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("confirmTotp", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("disableTotp", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("beginTotp", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("requestVerification", ctx);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function GET(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("verifyEmail", ctx);
|
||||
}
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("verifyEmail", ctx);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function POST(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("verifyPhone", ctx);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import type { createAuthHttpHandlers } from "../http/index.ts";
|
||||
|
||||
export type AuthHandlerName = keyof ReturnType<typeof createAuthHttpHandlers>;
|
||||
export type AuthRouteGroup =
|
||||
| "registration"
|
||||
| "login"
|
||||
| "verification"
|
||||
| "password"
|
||||
| "invitations"
|
||||
| "magicLink"
|
||||
| "otp"
|
||||
| "mfa"
|
||||
| "sessions"
|
||||
| "impersonation"
|
||||
| "passkeys";
|
||||
|
||||
export interface AuthRouteDefinition {
|
||||
path: string;
|
||||
group: AuthRouteGroup;
|
||||
handler: AuthHandlerName;
|
||||
methods: readonly ("GET" | "POST")[];
|
||||
}
|
||||
|
||||
/** Single source of truth for package-contributed auth endpoints. */
|
||||
export const AUTH_ROUTE_DEFINITIONS = [
|
||||
{ path: "/api/auth/register", group: "registration", handler: "register", methods: ["POST"] },
|
||||
{ path: "/api/auth/login", group: "login", handler: "login", methods: ["POST"] },
|
||||
{ path: "/api/auth/logout", group: "login", handler: "logout", methods: ["POST"] },
|
||||
{
|
||||
path: "/api/auth/verification/request",
|
||||
group: "verification",
|
||||
handler: "requestVerification",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/verify/email",
|
||||
group: "verification",
|
||||
handler: "verifyEmail",
|
||||
methods: ["GET", "POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/verify/phone",
|
||||
group: "verification",
|
||||
handler: "verifyPhone",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/password/request",
|
||||
group: "password",
|
||||
handler: "requestPasswordReset",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/password/reset",
|
||||
group: "password",
|
||||
handler: "resetPassword",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/password/change",
|
||||
group: "password",
|
||||
handler: "changePassword",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/invitations/accept",
|
||||
group: "invitations",
|
||||
handler: "acceptInvitation",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/magic-link/request",
|
||||
group: "magicLink",
|
||||
handler: "requestMagicLink",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/magic-link",
|
||||
group: "magicLink",
|
||||
handler: "consumeMagicLink",
|
||||
methods: ["GET", "POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/otp/login/request",
|
||||
group: "otp",
|
||||
handler: "requestOtpLogin",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/otp/login/complete",
|
||||
group: "otp",
|
||||
handler: "completeOtpLogin",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{ path: "/api/auth/otp", group: "otp", handler: "issueOtp", methods: ["POST"] },
|
||||
{
|
||||
path: "/api/auth/otp/verify",
|
||||
group: "otp",
|
||||
handler: "verifyOtp",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{ path: "/api/auth/totp/setup", group: "mfa", handler: "beginTotp", methods: ["POST"] },
|
||||
{
|
||||
path: "/api/auth/totp/confirm",
|
||||
group: "mfa",
|
||||
handler: "confirmTotp",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/totp/disable",
|
||||
group: "mfa",
|
||||
handler: "disableTotp",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/recovery-codes",
|
||||
group: "mfa",
|
||||
handler: "recoveryCodes",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{ path: "/api/auth/mfa/otp", group: "mfa", handler: "beginMfaOtp", methods: ["POST"] },
|
||||
{
|
||||
path: "/api/auth/mfa/complete",
|
||||
group: "mfa",
|
||||
handler: "completeMfa",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{ path: "/api/auth/sessions", group: "sessions", handler: "sessions", methods: ["GET"] },
|
||||
{
|
||||
path: "/api/auth/sessions/revoke",
|
||||
group: "sessions",
|
||||
handler: "revokeSession",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/impersonation/start",
|
||||
group: "impersonation",
|
||||
handler: "startImpersonation",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/impersonation/stop",
|
||||
group: "impersonation",
|
||||
handler: "stopImpersonation",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/passkeys/register/options",
|
||||
group: "passkeys",
|
||||
handler: "passkeyRegistrationOptions",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/passkeys/register/verify",
|
||||
group: "passkeys",
|
||||
handler: "passkeyRegistrationVerify",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/passkeys/login/options",
|
||||
group: "passkeys",
|
||||
handler: "passkeyAuthenticationOptions",
|
||||
methods: ["POST"],
|
||||
},
|
||||
{
|
||||
path: "/api/auth/passkeys/login/verify",
|
||||
group: "passkeys",
|
||||
handler: "passkeyAuthenticationVerify",
|
||||
methods: ["POST"],
|
||||
},
|
||||
] as const satisfies readonly AuthRouteDefinition[];
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Middleware } from "@wrnexus/core";
|
||||
import { authSession } from "../middleware.ts";
|
||||
import { getDefaultAuthEngine, hasDefaultAuthEngine } from "../runtime.ts";
|
||||
|
||||
/** Package middleware: hydrates auth state when a default engine is configured. */
|
||||
const middleware: Middleware = async (ctx, next) => {
|
||||
if (!hasDefaultAuthEngine()) return next();
|
||||
return authSession(getDefaultAuthEngine())(ctx, next);
|
||||
};
|
||||
|
||||
export default middleware;
|
||||
Reference in New Issue
Block a user