feat: centralize application framework primitives
Quality / quality (ubuntu-latest) (push) Failing after 14m38s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-22 23:07:46 +05:30
parent 96e082b943
commit a3ddd39b7b
73 changed files with 1429 additions and 84 deletions
+94 -2
View File
@@ -10,6 +10,8 @@ import {
} from "../middleware.ts";
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "../validation.ts";
import type { AuthSessionVerificationHandler } from "../types.ts";
import { completeAuth, startAuth, type OAuthProvider } from "@wrnexus/oauth";
import { assignDefaultAuthzRoles } from "@wrnexus/authz";
function text(value: unknown): string {
return typeof value === "string" ? value : value == null ? "" : String(value);
@@ -50,6 +52,8 @@ export interface AuthHttpOptions {
schemas?: AuthSchemaOverrides | AuthSchemaSet;
passkey?: AuthPasskeyHttpOptions;
onSessionVerification?: AuthSessionVerificationHandler;
oauth?: Record<string, OAuthProvider>;
oauthMfaPath?: string;
}
export function createAuthHttpHandlers(options: AuthHttpOptions) {
@@ -59,6 +63,17 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
const onSignedOut = engine.onSignedOut;
const onSuccessfulSignUp = engine.onSuccessfulSignUp;
function oauthProvider(ctx: Context): OAuthProvider | undefined {
return options.oauth?.[String(ctx.params.provider ?? "").toLowerCase()];
}
function oauthRedirectUri(ctx: Context, provider: OAuthProvider): string {
return new URL(
`/api/auth/oauth/${encodeURIComponent(provider.name)}/callback`,
options.baseUrl ?? ctx.url.origin,
).toString();
}
function signupRedirect(ctx: Context, value: string | undefined, fallback: string): Response {
const path = safeAuthReturnTo(value, ctx.url.origin) ?? fallback;
return Response.redirect(new URL(path, ctx.url), 303);
@@ -80,6 +95,71 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
}
return {
async oauthProviders(ctx: Context): Promise<Response> {
return json({
ok: true,
providers: Object.values(options.oauth ?? {}).map((provider) => ({
id: provider.name,
name: provider.name,
label: `Continue with ${provider.name.charAt(0).toUpperCase()}${provider.name.slice(1)}`,
href: `/api/auth/oauth/${encodeURIComponent(provider.name)}?returnTo=${encodeURIComponent(
safeAuthReturnTo(ctx.url.searchParams.get("returnTo") ?? undefined, ctx.url.origin) ??
"/",
)}`,
})),
});
},
async startOAuth(ctx: Context): Promise<Response> {
const provider = oauthProvider(ctx);
if (!provider) return json({ ok: false, error: "OAuth provider not configured" }, 404);
const returnTo =
safeAuthReturnTo(ctx.url.searchParams.get("returnTo") ?? undefined, ctx.url.origin) ?? "/";
const started = await startAuth(provider, { redirectUri: oauthRedirectUri(ctx, provider) });
ctx.cookies.transaction(`wrnexus.oauth.${provider.name}`).set({
state: started.state,
verifier: started.verifier,
returnTo,
});
return Response.redirect(started.url, 302);
},
async completeOAuth(ctx: Context): Promise<Response> {
const provider = oauthProvider(ctx);
if (!provider) return json({ ok: false, error: "OAuth provider not configured" }, 404);
const transaction = ctx.cookies
.transaction<{
state: string;
verifier: string;
returnTo: string;
}>(`wrnexus.oauth.${provider.name}`)
.consume();
const state = ctx.url.searchParams.get("state");
const code = ctx.url.searchParams.get("code");
if (!transaction || !state || transaction.state !== state || !code) {
return json({ ok: false, error: "OAuth transaction is invalid or expired" }, 400);
}
const completed = await completeAuth(provider, {
code,
verifier: transaction.verifier,
redirectUri: oauthRedirectUri(ctx, provider),
});
const result = await engine.loginWithOAuth(
provider.name,
completed.profile,
completed.tokens,
);
if (result.code === "mfa-required" && result.mfaToken) {
ctx.cookies.transaction("wrnexus.auth.mfa", { sameSite: "Lax", maxAge: 300 }).set({
mfaToken: result.mfaToken,
returnTo: transaction.returnTo,
});
return Response.redirect(new URL(options.oauthMfaPath ?? "/two-factor", ctx.url), 303);
}
if (!result.ok || !result.session || !result.user) return json(result, 401);
establishAuthSession(ctx, result.session, result.user);
if (onSignedIn) return onSignedIn(ctx, transaction.returnTo);
return Response.redirect(new URL(transaction.returnTo, ctx.url), 303);
},
async verifySession(ctx: Context): Promise<Response> {
const user = getAuthUser(ctx);
if (options.onSessionVerification) {
@@ -110,6 +190,8 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
});
if (!result.ok || !result.user) return json(result, 400);
await assignDefaultAuthzRoles(result.user.id, "signup");
const action = await onSuccessfulSignUp?.(ctx, result.user);
if (action instanceof Response) return action;
if (action?.autoSignIn) {
@@ -230,6 +312,7 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
password: text(input.password) || undefined,
displayName: text(input.displayName) || undefined,
});
if (result.ok && result.user) await assignDefaultAuthzRoles(result.user.id, "invitation");
return json(result, result.ok ? 200 : 400);
},
@@ -403,12 +486,18 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
const validation = await parseBody(schemas.mfaComplete, ctx.req);
if (!validation.ok) return validation.response;
const input = validation.value;
const continuation = ctx.cookies
.transaction<{ mfaToken: string; returnTo?: string }>("wrnexus.auth.mfa", {
sameSite: "Lax",
maxAge: 300,
})
.consume();
const methodValue = text(input.method);
const method = ["totp", "recovery-code", "email-otp", "sms-otp"].includes(methodValue)
? (methodValue as "totp" | "recovery-code" | "email-otp" | "sms-otp")
: "totp";
const result = await engine.completeMfa({
mfaToken: text(input.mfaToken),
mfaToken: text(input.mfaToken) || continuation?.mfaToken || "",
method,
code: text(input.code),
challengeId: text(input.challengeId) || undefined,
@@ -421,7 +510,10 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
if (!result.ok || !result.session || !result.user) return json(result, 400);
establishAuthSession(ctx, result.session, result.user);
if (onSignedIn) {
return onSignedIn(ctx, safeAuthReturnTo(text(input.returnTo) || undefined, ctx.url.origin));
return onSignedIn(
ctx,
safeAuthReturnTo(text(input.returnTo) || continuation?.returnTo, ctx.url.origin),
);
}
return json(result);
},