@withstudiocms/auth-kit
Version:
Utilities for managing authentication
88 lines (87 loc) • 3.53 kB
JavaScript
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
};