import type { AstroIntegration } from "astro"; import { fileURLToPath } from "node:url"; export type EnvString = { env: string; fallback?: string; }; export type RequiredSecret = { env: string; }; export type OidcIntegrationOptions = { issuer: EnvString; clientId: EnvString; scopes?: string; // default "openid email profile" routes?: { login?: string; callback?: string; logout?: string; logoutCallback?: string; }; // defaults: /login, /oidc/callback, /logout, /logout/callback redirectUri?: { mode: "infer-from-request" } | { absolute: string }; cookie: { name?: string; sameSite?: "Lax" | "Strict" | "None"; secure?: boolean; domain?: string; path?: string; signingSecret: RequiredSecret; maxAgeSec?: number; }; protected?: string[]; // patterns to guard, e.g., ["/app/*", "/me"] }; export type OidcInjectedOptions = { issuerEnv: string; issuerFallback?: string; clientIdEnv: string; clientIdFallback?: string; scopes: string; routes: { login: string; callback: string; logout: string; logoutCallback: string }; redirectUri: { mode: "infer-from-request" } | { absolute: string }; cookie: { name: string; sameSite: "Lax" | "Strict" | "None"; secure: boolean; path: string; signingSecretEnv: string; domain?: string; maxAgeSec?: number; }; protected: string[]; }; function requireEnvName(name: string, label: string): string { if (typeof name !== "string" || name.trim().length === 0) throw new Error(`@resuely/astro-oidc-rp: ${label} must be a non-empty string`); return name; } function resolveOptions(user: OidcIntegrationOptions): OidcInjectedOptions { const routes = { login: user.routes?.login ?? "/login", callback: user.routes?.callback ?? "/oidc/callback", logout: user.routes?.logout ?? "/logout", logoutCallback: user.routes?.logoutCallback ?? `${user.routes?.logout ?? "/logout"}/callback`, }; const cookie = { name: user.cookie?.name ?? "oidc_session", sameSite: user.cookie?.sameSite ?? "Lax", secure: user.cookie?.secure ?? true, path: user.cookie?.path ?? "/", signingSecretEnv: requireEnvName( user.cookie?.signingSecret?.env, "cookie.signingSecret.env", ), domain: user.cookie?.domain, maxAgeSec: user.cookie?.maxAgeSec, }; return { issuerEnv: requireEnvName(user.issuer.env, "issuer.env"), issuerFallback: typeof user.issuer.fallback === "string" ? user.issuer.fallback : undefined, clientIdEnv: requireEnvName(user.clientId.env, "clientId.env"), clientIdFallback: typeof user.clientId.fallback === "string" ? user.clientId.fallback : undefined, scopes: user.scopes ?? "openid email profile", routes, redirectUri: user.redirectUri ?? { mode: "infer-from-request" }, cookie, protected: user.protected ?? [], }; } export default function resuelyOidc( options: OidcIntegrationOptions, ): AstroIntegration { const resolved = resolveOptions(options); return { name: "@resuely/astro-oidc-rp", hooks: { "astro:config:setup": ({ injectRoute, addMiddleware, updateConfig }) => { // Provide runtime options to middleware/routes via Vite define replacement updateConfig({ vite: { define: { __RESUELY_OIDC_OPTIONS: JSON.stringify(resolved), }, }, }); // Add middleware (run early to populate locals and guard protected patterns) addMiddleware({ entrypoint: fileURLToPath(new URL("./middleware.js", import.meta.url)), order: "pre", }); // Inject routes injectRoute({ pattern: resolved.routes.login, entrypoint: fileURLToPath(new URL("./routes/login.js", import.meta.url)), }); injectRoute({ pattern: resolved.routes.callback, entrypoint: fileURLToPath(new URL("./routes/callback.js", import.meta.url)), }); injectRoute({ pattern: resolved.routes.logout, entrypoint: fileURLToPath(new URL("./routes/logout.js", import.meta.url)), }); injectRoute({ pattern: resolved.routes.logoutCallback, entrypoint: fileURLToPath(new URL("./routes/logout.js", import.meta.url)), }); }, }, }; }