215 lines
3.9 KiB
TypeScript
215 lines
3.9 KiB
TypeScript
/**
|
|
* Configuration schema for @emdash-cms/auth
|
|
*/
|
|
|
|
import { z } from "zod";
|
|
|
|
import type { RoleName } from "./types.js";
|
|
|
|
/** Matches http(s) scheme at start of URL */
|
|
const HTTP_SCHEME_RE = /^https?:\/\//i;
|
|
|
|
/** Validates that a URL string uses http or https scheme. Rejects javascript:/data: URI XSS vectors. */
|
|
const httpUrl = z
|
|
.string()
|
|
.url()
|
|
.refine((url) => HTTP_SCHEME_RE.test(url), "URL must use http or https");
|
|
|
|
/**
|
|
* OAuth provider configuration
|
|
*/
|
|
const oauthProviderSchema = z.object({
|
|
clientId: z.string(),
|
|
clientSecret: z.string(),
|
|
});
|
|
|
|
/**
|
|
* Full auth configuration schema
|
|
*/
|
|
export const authConfigSchema = z.object({
|
|
/**
|
|
* Secret key for encrypting tokens and session data.
|
|
* Generate with: `emdash auth secret`
|
|
*/
|
|
secret: z.string().min(32, "Auth secret must be at least 32 characters"),
|
|
|
|
/**
|
|
* Passkey (WebAuthn) configuration
|
|
*/
|
|
passkeys: z
|
|
.object({
|
|
/**
|
|
* Relying party name shown to users during passkey registration
|
|
*/
|
|
rpName: z.string(),
|
|
/**
|
|
* Relying party ID (domain). Defaults to the hostname from baseUrl.
|
|
*/
|
|
rpId: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
|
|
/**
|
|
* Self-signup configuration
|
|
*/
|
|
selfSignup: z
|
|
.object({
|
|
/**
|
|
* Email domains allowed to self-register
|
|
*/
|
|
domains: z.array(z.string()),
|
|
/**
|
|
* Default role for self-registered users
|
|
*/
|
|
defaultRole: z.enum(["subscriber", "contributor", "author"] as const).default("contributor"),
|
|
})
|
|
.optional(),
|
|
|
|
/**
|
|
* OAuth provider configurations (for "Login with X")
|
|
*/
|
|
oauth: z
|
|
.object({
|
|
github: oauthProviderSchema.optional(),
|
|
google: oauthProviderSchema.optional(),
|
|
})
|
|
.optional(),
|
|
|
|
/**
|
|
* Configure EmDash as an OAuth provider
|
|
*/
|
|
provider: z
|
|
.object({
|
|
enabled: z.boolean(),
|
|
/**
|
|
* Issuer URL for OIDC. Defaults to site URL.
|
|
*/
|
|
issuer: httpUrl.optional(),
|
|
})
|
|
.optional(),
|
|
|
|
/**
|
|
* Enterprise SSO configuration
|
|
*/
|
|
sso: z
|
|
.object({
|
|
enabled: z.boolean(),
|
|
})
|
|
.optional(),
|
|
|
|
/**
|
|
* Session configuration
|
|
*/
|
|
session: z
|
|
.object({
|
|
/**
|
|
* Session max age in seconds. Default: 30 days
|
|
*/
|
|
maxAge: z.number().default(30 * 24 * 60 * 60),
|
|
/**
|
|
* Extend session on activity. Default: true
|
|
*/
|
|
sliding: z.boolean().default(true),
|
|
})
|
|
.optional(),
|
|
});
|
|
|
|
export type AuthConfig = z.infer<typeof authConfigSchema>;
|
|
|
|
/**
|
|
* Validated and resolved auth configuration
|
|
*/
|
|
export interface ResolvedAuthConfig {
|
|
secret: string;
|
|
baseUrl: string;
|
|
siteName: string;
|
|
|
|
passkeys: {
|
|
rpName: string;
|
|
rpId: string;
|
|
origin: string;
|
|
};
|
|
|
|
selfSignup?: {
|
|
domains: string[];
|
|
defaultRole: RoleName;
|
|
};
|
|
|
|
oauth?: {
|
|
github?: {
|
|
clientId: string;
|
|
clientSecret: string;
|
|
};
|
|
google?: {
|
|
clientId: string;
|
|
clientSecret: string;
|
|
};
|
|
};
|
|
|
|
provider?: {
|
|
enabled: boolean;
|
|
issuer: string;
|
|
};
|
|
|
|
sso?: {
|
|
enabled: boolean;
|
|
};
|
|
|
|
session: {
|
|
maxAge: number;
|
|
sliding: boolean;
|
|
};
|
|
}
|
|
|
|
const selfSignupRoleMap: Record<"subscriber" | "contributor" | "author", RoleName> = {
|
|
subscriber: "SUBSCRIBER",
|
|
contributor: "CONTRIBUTOR",
|
|
author: "AUTHOR",
|
|
};
|
|
|
|
/**
|
|
* Resolve auth configuration with defaults
|
|
*/
|
|
export function resolveConfig(
|
|
config: AuthConfig,
|
|
baseUrl: string,
|
|
siteName: string,
|
|
): ResolvedAuthConfig {
|
|
const url = new URL(baseUrl);
|
|
|
|
return {
|
|
secret: config.secret,
|
|
baseUrl,
|
|
siteName,
|
|
|
|
passkeys: {
|
|
rpName: config.passkeys?.rpName ?? siteName,
|
|
rpId: config.passkeys?.rpId ?? url.hostname,
|
|
origin: url.origin,
|
|
},
|
|
|
|
selfSignup: config.selfSignup
|
|
? {
|
|
domains: config.selfSignup.domains.map((d) => d.toLowerCase()),
|
|
defaultRole: selfSignupRoleMap[config.selfSignup.defaultRole],
|
|
}
|
|
: undefined,
|
|
|
|
oauth: config.oauth,
|
|
|
|
provider: config.provider
|
|
? {
|
|
enabled: config.provider.enabled,
|
|
issuer: config.provider.issuer ?? baseUrl,
|
|
}
|
|
: undefined,
|
|
|
|
sso: config.sso,
|
|
|
|
session: {
|
|
maxAge: config.session?.maxAge ?? 30 * 24 * 60 * 60,
|
|
sliding: config.session?.sliding ?? true,
|
|
},
|
|
};
|
|
}
|