feat(identity): define self-hosted identity configuration
This commit is contained in:
Generated
+355
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
Reference in New Issue
Block a user