first commit
This commit is contained in:
214
packages/auth/src/config.ts
Normal file
214
packages/auth/src/config.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Configuration schema for @emdashcms/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,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user