feat(identity): define self-hosted identity configuration

This commit is contained in:
Jesse_Chen
2026-07-21 11:13:21 +08:00
parent be83b17e02
commit d3a98d54a4
5 changed files with 621 additions and 0 deletions
+355
View File
@@ -13,6 +13,7 @@
"@supabase/ssr": "^0.12.3",
"@supabase/supabase-js": "^2.110.5",
"@tailwindcss/postcss": "^4.3.2",
"better-auth": "1.6.23",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
@@ -580,6 +581,33 @@
}
}
},
"node_modules/@better-auth/utils": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.2.tgz",
"integrity": "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^2.0.1"
}
},
"node_modules/@better-auth/utils/node_modules/@noble/hashes": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@better-fetch/fetch": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.3.1.tgz",
"integrity": "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==",
"license": "MIT"
},
"node_modules/@date-fns/tz": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz",
@@ -2305,6 +2333,15 @@
"node": ">=12.4.0"
}
},
"node_modules/@opentelemetry/semantic-conventions": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
"integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@posthog/core": {
"version": "1.40.2",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.40.2.tgz",
@@ -4084,6 +4121,282 @@
"node": ">=6.0.0"
}
},
"node_modules/better-auth": {
"version": "1.6.23",
"resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.23.tgz",
"integrity": "sha512-4vOaRd9UiKGKm9R+ej0jjU1es3MiJIiNc9Qq3VCnYqOZ4/nb5272QqTxWYoDxyUXl5x6A2x2we5KZKQO9teTQQ==",
"license": "MIT",
"dependencies": {
"@better-auth/core": "1.6.23",
"@better-auth/drizzle-adapter": "1.6.23",
"@better-auth/kysely-adapter": "1.6.23",
"@better-auth/memory-adapter": "1.6.23",
"@better-auth/mongo-adapter": "1.6.23",
"@better-auth/prisma-adapter": "1.6.23",
"@better-auth/telemetry": "1.6.23",
"@better-auth/utils": "0.4.2",
"@better-fetch/fetch": "1.3.1",
"@noble/ciphers": "^2.1.1",
"@noble/hashes": "^2.0.1",
"better-call": "1.3.7",
"defu": "^6.1.4",
"jose": "^6.1.3",
"kysely": "^0.28.17 || ^0.29.0",
"nanostores": "^1.1.1",
"zod": "^4.3.6"
},
"peerDependencies": {
"@lynx-js/react": "*",
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
"@sveltejs/kit": "^2.0.0",
"@tanstack/react-start": "^1.0.0",
"@tanstack/solid-start": "^1.0.0",
"better-sqlite3": "^12.0.0",
"drizzle-kit": ">=0.31.4",
"drizzle-orm": "^0.45.2",
"mongodb": "^6.0.0 || ^7.0.0",
"mysql2": "^3.0.0",
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
"pg": "^8.0.0",
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"solid-js": "^1.0.0",
"svelte": "^4.0.0 || ^5.0.0",
"vitest": "^2.0.0 || ^3.0.0 || ^4.0.0",
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"@lynx-js/react": {
"optional": true
},
"@prisma/client": {
"optional": true
},
"@sveltejs/kit": {
"optional": true
},
"@tanstack/react-start": {
"optional": true
},
"@tanstack/solid-start": {
"optional": true
},
"better-sqlite3": {
"optional": true
},
"drizzle-kit": {
"optional": true
},
"drizzle-orm": {
"optional": true
},
"mongodb": {
"optional": true
},
"mysql2": {
"optional": true
},
"next": {
"optional": true
},
"pg": {
"optional": true
},
"prisma": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
},
"solid-js": {
"optional": true
},
"svelte": {
"optional": true
},
"vitest": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/better-auth/node_modules/@better-auth/core": {
"version": "1.6.23",
"resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.23.tgz",
"integrity": "sha512-beEhOs0uVeOxYOZKUfIEBd/nQV2Bd4/6wyLxZ0OFkn6CMTK2Vi+hXuZLnyPBeB6RdHpebEoJWiHqwHxBIxgPDQ==",
"license": "MIT",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.39.0",
"@standard-schema/spec": "^1.1.0",
"zod": "^4.3.6"
},
"peerDependencies": {
"@better-auth/utils": "0.4.2",
"@better-fetch/fetch": "1.3.1",
"@cloudflare/workers-types": ">=4",
"@opentelemetry/api": "^1.9.0",
"better-call": "1.3.7",
"jose": "^6.1.0",
"kysely": "^0.28.5 || ^0.29.0",
"nanostores": "^1.0.1"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
},
"@opentelemetry/api": {
"optional": true
}
}
},
"node_modules/better-auth/node_modules/@better-auth/drizzle-adapter": {
"version": "1.6.23",
"resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.23.tgz",
"integrity": "sha512-2+/PTVfIP9E7iz6af8TB3lhnowHUj9ljC66kECmHaFEdUqPgzHoWux9epotKwO7XDg2ui4ttWQ8CMeNFLvQeKQ==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "^1.6.23",
"@better-auth/utils": "0.4.2",
"drizzle-orm": "^0.45.2"
},
"peerDependenciesMeta": {
"drizzle-orm": {
"optional": true
}
}
},
"node_modules/better-auth/node_modules/@better-auth/kysely-adapter": {
"version": "1.6.23",
"resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.6.23.tgz",
"integrity": "sha512-zbNJsMbG09exfkGyvFqBLLqWoMPAUWjxCuUnEK5AsjbYoZeIjj/QGZgdf4CapVWryKxjA9Q6Jlr6fbiPpC3VAg==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "^1.6.23",
"@better-auth/utils": "0.4.2",
"kysely": "^0.28.17 || ^0.29.0"
},
"peerDependenciesMeta": {
"kysely": {
"optional": true
}
}
},
"node_modules/better-auth/node_modules/@better-auth/memory-adapter": {
"version": "1.6.23",
"resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.6.23.tgz",
"integrity": "sha512-krIiR0pIVkaKlAzm690n5bcMW4NGbqeMg0HQSD9fz/KcQF/eWLqcq9gG/BhHTj2i/y96qH+W5JWPmaSOS5iTgQ==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "^1.6.23",
"@better-auth/utils": "0.4.2"
}
},
"node_modules/better-auth/node_modules/@better-auth/mongo-adapter": {
"version": "1.6.23",
"resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.6.23.tgz",
"integrity": "sha512-7+QdevitGlKBbP6JbiSk5SBnzPsKV/mDrQBGBn8hwByQLeJwqpqbuBPw7ZI8vzUlFfAAnyFiqwP3Eb8mxnp7pA==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "^1.6.23",
"@better-auth/utils": "0.4.2",
"mongodb": "^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"mongodb": {
"optional": true
}
}
},
"node_modules/better-auth/node_modules/@better-auth/prisma-adapter": {
"version": "1.6.23",
"resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.6.23.tgz",
"integrity": "sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "^1.6.23",
"@better-auth/utils": "0.4.2",
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"@prisma/client": {
"optional": true
},
"prisma": {
"optional": true
}
}
},
"node_modules/better-auth/node_modules/@better-auth/telemetry": {
"version": "1.6.23",
"resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.6.23.tgz",
"integrity": "sha512-/R2Kb+z2BpDOOWwVHqOk+c0VNpuwfCv4Hp5Yr9003WIZPax/zyNraGLB9CFE8qF2gZW8Dsz419k4I8CPrGzpDA==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "^1.6.23",
"@better-auth/utils": "0.4.2",
"@better-fetch/fetch": "1.3.1"
}
},
"node_modules/better-auth/node_modules/@noble/ciphers": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.2.0.tgz",
"integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/better-auth/node_modules/@noble/hashes": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/better-auth/node_modules/better-call": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.7.tgz",
"integrity": "sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w==",
"license": "MIT",
"dependencies": {
"@better-auth/utils": "^0.4.0",
"@better-fetch/fetch": "^1.1.21",
"rou3": "^0.7.12",
"set-cookie-parser": "^3.0.1"
},
"peerDependencies": {
"zod": "^4.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/better-auth/node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/body-parser": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
@@ -4680,6 +4993,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/defu": {
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"license": "MIT"
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -7274,6 +7593,15 @@
"node": ">=0.10.0"
}
},
"node_modules/kysely": {
"version": "0.29.4",
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.29.4.tgz",
"integrity": "sha512-y5mVgQNkMbs1eK9Xyc0pmNdabN2wHhRYY/5r4W5HrUT1rYCEPeVNSj1RUJeSDKT3U0p+mXCvLgkrFuIafYI6BA==",
"license": "MIT",
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -8624,6 +8952,21 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/nanostores": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.4.1.tgz",
"integrity": "sha512-PGd3uPojJB9Z07d5NX3Db/SOSBbyy3wLMUGq0GpnEEJfVzY9mq7daPMAZ3jObV5D3Jn+YKND636eI5ULg7F80Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"engines": {
"node": "^20.0.0 || >=22.0.0"
}
},
"node_modules/napi-postinstall": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
@@ -9713,6 +10056,12 @@
"node": ">=0.10.0"
}
},
"node_modules/rou3": {
"version": "0.7.12",
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz",
"integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==",
"license": "MIT"
},
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@@ -9894,6 +10243,12 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/set-cookie-parser": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz",
"integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==",
"license": "MIT"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+1
View File
@@ -21,6 +21,7 @@
"@supabase/ssr": "^0.12.3",
"@supabase/supabase-js": "^2.110.5",
"@tailwindcss/postcss": "^4.3.2",
"better-auth": "1.6.23",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
+115
View File
@@ -0,0 +1,115 @@
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;
adminSecret: string;
resendApiKey: string;
resendFrom: string;
}
export type IdentityConfig =
| SupabaseIdentityConfig
| SelfHostedIdentityConfig;
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;
}
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");
}
const userOrigin = readOrigin(env, "AUTH_USER_ORIGIN");
const adminOrigin = readOrigin(env, "AUTH_ADMIN_ORIGIN");
if (userOrigin === adminOrigin) {
throw new Error("user and admin origins must be different");
}
const userSecret = readSecret(env, "BETTER_AUTH_USER_SECRET");
const adminSecret = readSecret(env, "BETTER_AUTH_ADMIN_SECRET");
if (userSecret === adminSecret) {
throw new Error("user and admin secrets must be different");
}
return {
provider,
databaseUrl: readPostgresUrl(env),
userOrigin,
adminOrigin,
userSecret,
adminSecret,
resendApiKey: required(env, "RESEND_API_KEY"),
resendFrom: readSender(env),
};
}
@@ -0,0 +1,31 @@
export type IdentitySurface = "user" | "admin";
export type EmailOtpType =
| "sign-in"
| "email-verification"
| "forget-password";
export interface EmailOtpMessage {
email: string;
otp: string;
type: EmailOtpType;
idempotencyKey: string;
}
export interface EmailOtpSender {
send(message: EmailOtpMessage): Promise<void>;
}
export interface IdentityUser {
id: string;
email: string;
emailVerified: boolean;
name: string;
image: string | null;
role: string[];
}
export interface IdentitySession {
user: IdentityUser;
expiresAt: Date;
}
+119
View File
@@ -0,0 +1,119 @@
import assert from "node:assert/strict";
import test from "node:test";
import { readIdentityConfig } from "../src/modules/identity/config.ts";
const selfHostedEnvironment = {
AUTH_PROVIDER: "self-hosted",
IDENTITY_DATABASE_URL:
"postgresql://identity_runtime:test-password@postgres:5432/jyotisha?options=-csearch_path%3Didentity",
AUTH_USER_ORIGIN: "https://staging.jyotisha.chat",
AUTH_ADMIN_ORIGIN: "https://admin.staging.jyotisha.chat",
BETTER_AUTH_USER_SECRET: "user-secret-that-is-at-least-32-bytes-long",
BETTER_AUTH_ADMIN_SECRET: "admin-secret-that-is-at-least-32-bytes-long",
RESEND_API_KEY: "re_test_key_that_must_not_be_printed",
RESEND_FROM_EMAIL: "Jyotisha Staging <login@staging.jyotisha.chat>",
};
test("identity provider defaults to supabase without self-hosted settings", () => {
assert.deepEqual(readIdentityConfig({}), { provider: "supabase" });
});
test("identity config accepts a complete self-hosted environment", () => {
const config = readIdentityConfig(selfHostedEnvironment);
assert.equal(config.provider, "self-hosted");
if (config.provider !== "self-hosted") {
assert.fail("expected self-hosted identity configuration");
}
assert.equal(config.userOrigin, "https://staging.jyotisha.chat");
assert.equal(config.adminOrigin, "https://admin.staging.jyotisha.chat");
assert.equal(config.resendFrom, selfHostedEnvironment.RESEND_FROM_EMAIL);
});
test("identity config rejects unknown providers", () => {
assert.throws(
() => readIdentityConfig({ AUTH_PROVIDER: "firebase" }),
/AUTH_PROVIDER must be supabase or self-hosted/,
);
});
test("self-hosted identity reports missing keys without leaking configured secrets", () => {
const secret = "this-secret-must-never-appear-in-an-error";
assert.throws(
() =>
readIdentityConfig({
...selfHostedEnvironment,
BETTER_AUTH_USER_SECRET: secret,
RESEND_API_KEY: "",
}),
(error: unknown) => {
assert.ok(error instanceof Error);
assert.match(error.message, /RESEND_API_KEY is required/);
assert.doesNotMatch(error.message, new RegExp(secret));
return true;
},
);
});
test("self-hosted identity validates database URL, origins, secrets, and sender", () => {
const invalidCases: Array<[string, Record<string, string>, RegExp]> = [
[
"database URL",
{ IDENTITY_DATABASE_URL: "https://database.invalid" },
/IDENTITY_DATABASE_URL must be a PostgreSQL URL/,
],
[
"production HTTP origin",
{ AUTH_USER_ORIGIN: "http://staging.jyotisha.chat" },
/AUTH_USER_ORIGIN must use HTTPS/,
],
[
"origin path",
{ AUTH_ADMIN_ORIGIN: "https://admin.staging.jyotisha.chat/login" },
/AUTH_ADMIN_ORIGIN must be an origin without a path/,
],
[
"short secret",
{ BETTER_AUTH_ADMIN_SECRET: "too-short" },
/BETTER_AUTH_ADMIN_SECRET must be at least 32 characters/,
],
[
"shared secret",
{
BETTER_AUTH_ADMIN_SECRET:
selfHostedEnvironment.BETTER_AUTH_USER_SECRET,
},
/user and admin secrets must be different/,
],
[
"shared origin",
{ AUTH_ADMIN_ORIGIN: selfHostedEnvironment.AUTH_USER_ORIGIN },
/user and admin origins must be different/,
],
[
"invalid sender",
{ RESEND_FROM_EMAIL: "Jyotisha Staging" },
/RESEND_FROM_EMAIL must contain a valid email address/,
],
];
for (const [name, override, expected] of invalidCases) {
assert.throws(
() => readIdentityConfig({ ...selfHostedEnvironment, ...override }),
expected,
name,
);
}
});
test("localhost origins may use HTTP for local development", () => {
const config = readIdentityConfig({
...selfHostedEnvironment,
AUTH_USER_ORIGIN: "http://localhost:3000",
AUTH_ADMIN_ORIGIN: "http://admin.localhost:3000",
});
assert.equal(config.provider, "self-hosted");
});