UNPKG

@withstudiocms/auth-kit

Version:

Utilities for managing authentication

88 lines (87 loc) 3.53 kB
import { Brand, Context, Layer } from "@withstudiocms/effect"; import { ScryptConfigOptions } from "@withstudiocms/effect/scrypt"; import { defaultSessionConfig } from "./utils/session.js"; const PasswordModConfigFinal = Brand.nominal(); const AuthKitConfig = Brand.nominal(); function makePasswordModConfig({ CMS_ENCRYPTION_KEY }) { const normalizedKey = CMS_ENCRYPTION_KEY.trim(); if (normalizedKey.length === 0) { throw new Error("CMS_ENCRYPTION_KEY must be a non-empty base64 string"); } let raw; try { raw = typeof Buffer !== "undefined" ? Buffer.from(normalizedKey, "base64") : new Uint8Array( atob(normalizedKey).split("").map((c) => c.charCodeAt(0)) ); } catch { throw new Error("CMS_ENCRYPTION_KEY is not valid base64"); } if (raw.byteLength !== 16) { throw new Error(`CMS_ENCRYPTION_KEY must decode to 16 bytes, got ${raw.byteLength}`); } const clamp = (v, min, max) => Number.isSafeInteger(v) ? Math.min(max, Math.max(min, v)) : min; const env = (k) => typeof process !== "undefined" && process.env ? process.env[k] : void 0; const parsedN = Number.parseInt(env("SCRYPT_N") ?? "", 10); const parsedR = Number.parseInt(env("SCRYPT_R") ?? "", 10); const parsedP = Number.parseInt(env("SCRYPT_P") ?? "", 10); const toPowerOfTwo = (n) => 1 << Math.floor(Math.log2(n)); const baseN = clamp(parsedN, 16384, 1 << 20); const SCRYPT_N = toPowerOfTwo(baseN); const SCRYPT_R = clamp(parsedR, 8, 32); const SCRYPT_P = clamp(parsedP, 1, 16); const scrypt = ScryptConfigOptions({ encryptionKey: normalizedKey, keylen: 64, options: { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P } }); return PasswordModConfigFinal({ scrypt }); } class PasswordModOptions extends Context.Tag("PasswordModOptions")() { static Live = ({ CMS_ENCRYPTION_KEY }) => Layer.succeed(this, makePasswordModConfig({ CMS_ENCRYPTION_KEY })); } class AuthKitOptions extends Context.Tag("AuthKitOptions")() { /** * Creates a live instance of `AuthKitOptions` using the provided raw configuration. * * @param CMS_ENCRYPTION_KEY - The encryption key used for cryptographic operations. * @param session - session configuration overrides. * @param userTools - Tools or utilities related to user management. * @returns A Layer that provides the configured `AuthKitOptions`. * * @remarks * - Scrypt parameters (`N`, `r`, `p`) are read from environment variables (`SCRYPT_N`, `SCRYPT_R`, `SCRYPT_P`), * with sensible defaults and minimum values enforced for security. * - The session configuration merges defaults with any provided overrides. * - The returned Layer can be used for dependency injection in the application. */ static Live = ({ CMS_ENCRYPTION_KEY, session: _session, userTools }) => { const { scrypt } = makePasswordModConfig({ CMS_ENCRYPTION_KEY }); const session = { ...defaultSessionConfig, ..._session ?? {} }; if (typeof session.cookieName !== "string" || session.cookieName.trim() === "") { throw new Error("session.cookieName must be a non-empty string"); } if (!Number.isSafeInteger(session.expTime) || session.expTime <= 0) { throw new Error("session.expTime must be a positive integer (ms)"); } return Layer.succeed( this, this.of(AuthKitConfig({ CMS_ENCRYPTION_KEY, scrypt, session, userTools })) ); }; } export { AuthKitConfig, AuthKitOptions, PasswordModConfigFinal, PasswordModOptions, makePasswordModConfig };