Files
Jyotisha/frontend/src/modules/identity/config.ts
T
Jesse 2697d16ef1
Staging Backend Quality Gate / validate (push) Has been cancelled
Staging Backend Quality Gate / publish (push) Has been cancelled
feat(identity): test-only fixed OTP channel and rectification candidate cards
- IDENTITY_TEST_OTP env: when set to a 6-digit code, no real email is
  delivered; login page surfaces the fixed code so testers can register
  and sign in without a mailbox. Opt-in, never set in production.
- email-otp-login: test-channel notice with the pinned code.
- Rectification candidate list restyled with the project design system
  (warm canvas, action color, display serif, soft shadow, hover lift).
- Regression tests for config parsing, pinned OTP generation, login UI
  notice, and candidate card styles.
2026-08-10 19:38:27 +08:00

141 lines
4.0 KiB
TypeScript

type IdentityEnvironment = Record<string, string | undefined>;
export interface SupabaseIdentityConfig {
provider: "supabase";
}
export interface SelfHostedIdentityConfig {
provider: "self-hosted";
databaseUrl: string;
userOrigin: string;
adminOrigin: string;
userSecret: string;
resendApiKey: string;
resendFrom: string;
/** Fixed OTP accepted when IDENTITY_TEST_OTP is set (test channel only). */
testOtp: string | null;
}
export type IdentityConfig =
| SupabaseIdentityConfig
| SelfHostedIdentityConfig;
export function isSelfHostedIdentityEnabled(
env: IdentityEnvironment,
): boolean {
const value = env.SELF_HOSTED_IDENTITY_ENABLED?.trim() || "false";
if (value !== "true" && value !== "false") {
throw new Error("SELF_HOSTED_IDENTITY_ENABLED must be true or false");
}
return value === "true";
}
function required(env: IdentityEnvironment, key: string): string {
const value = env[key]?.trim();
if (!value) throw new Error(`${key} is required`);
return value;
}
function readPostgresUrl(env: IdentityEnvironment): string {
const value = required(env, "IDENTITY_DATABASE_URL");
if (!value.startsWith("postgresql://")) {
throw new Error("IDENTITY_DATABASE_URL must be a PostgreSQL URL");
}
try {
const url = new URL(value);
if (!url.hostname || !url.pathname || url.pathname === "/") {
throw new Error("invalid PostgreSQL URL");
}
} catch {
throw new Error("IDENTITY_DATABASE_URL must be a PostgreSQL URL");
}
return value;
}
function readOrigin(env: IdentityEnvironment, key: string): string {
const value = required(env, key);
let url: URL;
try {
url = new URL(value);
} catch {
throw new Error(`${key} must be a valid origin`);
}
const isLocalhost =
url.hostname === "localhost" || url.hostname.endsWith(".localhost");
if (url.protocol !== "https:" && !(isLocalhost && url.protocol === "http:")) {
throw new Error(`${key} must use HTTPS outside localhost`);
}
if (url.pathname !== "/" || url.search || url.hash || url.username || url.password) {
throw new Error(`${key} must be an origin without a path`);
}
return url.origin;
}
function readSecret(env: IdentityEnvironment, key: string): string {
const value = required(env, key);
if (value.length < 32) {
throw new Error(`${key} must be at least 32 characters`);
}
return value;
}
function readSender(env: IdentityEnvironment): string {
const value = required(env, "RESEND_FROM_EMAIL");
const match = value.match(/(?:^|<)([^<>\s]+@[^<>\s]+)(?:>|$)/);
if (!match) {
throw new Error("RESEND_FROM_EMAIL must contain a valid email address");
}
return value;
}
function readTestOtp(env: IdentityEnvironment): string | null {
const value = env.IDENTITY_TEST_OTP?.trim() || "";
if (!value) return null;
if (!/^\d{6}$/.test(value)) {
throw new Error("IDENTITY_TEST_OTP must be a 6-digit code when set");
}
return value;
}
export function readSelfHostedIdentityConfig(
env: IdentityEnvironment,
): SelfHostedIdentityConfig {
const userOrigin = readOrigin(env, "AUTH_USER_ORIGIN");
const adminOrigin = readOrigin(env, "ADMIN_USER_ORIGIN");
if (adminOrigin === userOrigin) {
throw new Error("ADMIN_USER_ORIGIN must differ from AUTH_USER_ORIGIN");
}
const userSecret = readSecret(env, "BETTER_AUTH_USER_SECRET");
return {
provider: "self-hosted",
databaseUrl: readPostgresUrl(env),
userOrigin,
adminOrigin,
userSecret,
resendApiKey: required(env, "RESEND_API_KEY"),
resendFrom: readSender(env),
testOtp: readTestOtp(env),
};
}
export function readIdentityConfig(
env: IdentityEnvironment,
): IdentityConfig {
const provider = env.AUTH_PROVIDER?.trim() || "supabase";
if (provider === "supabase") return { provider };
if (provider !== "self-hosted") {
throw new Error("AUTH_PROVIDER must be supabase or self-hosted");
}
if (!isSelfHostedIdentityEnabled(env)) {
throw new Error(
"SELF_HOSTED_IDENTITY_ENABLED must be true when AUTH_PROVIDER is self-hosted",
);
}
return readSelfHostedIdentityConfig(env);
}