import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { randomUUID, timingSafeEqual, createHmac } from "node:crypto";
const SESSION_COOKIE_NAME = "inkos_studio_session";
const DEFAULT_SESSION_SECRET = "inkos-studio-default-session-secret";
const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
const USERNAME_PATTERN = /^[a-z0-9_-]{3,32}$/;
export class AuthError extends Error {
    status;
    code;
    constructor(status, code, message) {
        super(message);
        this.status = status;
        this.code = code;
        this.name = "AuthError";
    }
}
export class AuthRuntime {
    usersPath;
    secureCookie;
    sessionSecret;
    constructor(options) {
        this.sessionSecret = options.sessionSecret || DEFAULT_SESSION_SECRET;
        this.usersPath = join(options.dataRoot, "platform", "users.json");
        this.secureCookie = options.publicUrl?.startsWith("https://") ?? false;
    }
    async listUsers() {
        return (await this.readUsers()).users;
    }
    async createUser(args) {
        validateUsername(args.username);
        validatePassword(args.password);
        const role = validateStudioUserRole(args.role ?? "user");
        const now = new Date().toISOString();
        const file = await this.readUsers();
        if (file.users.some((user) => user.username === args.username)) {
            throw new Error(`User already exists: ${args.username}`);
        }
        const user = {
            id: `usr_${randomUUID().replace(/-/g, "").slice(0, 20)}`,
            username: args.username,
            password: args.password,
            passwordResetRequired: false,
            role,
            disabled: false,
            createdAt: now,
            updatedAt: now,
        };
        await this.writeUsers({ users: [...file.users, user] });
        return user;
    }
    async resetPassword(username, password) {
        validatePassword(password);
        const file = await this.readUsers();
        let updated;
        const users = file.users.map((user) => {
            if (user.username !== username)
                return user;
            updated = {
                ...user,
                password,
                passwordResetRequired: false,
                updatedAt: new Date().toISOString(),
            };
            return updated;
        });
        if (!updated)
            throw new Error(`User not found: ${username}`);
        await this.writeUsers({ users });
        return updated;
    }
    async setDisabled(username, disabled) {
        const file = await this.readUsers();
        let updated;
        const users = file.users.map((user) => {
            if (user.username !== username)
                return user;
            updated = { ...user, disabled, updatedAt: new Date().toISOString() };
            return updated;
        });
        if (!updated)
            throw new Error(`User not found: ${username}`);
        await this.writeUsers({ users });
        return updated;
    }
    async setRole(username, role) {
        const nextRole = validateStudioUserRole(role);
        const file = await this.readUsers();
        let updated;
        const users = file.users.map((user) => {
            if (user.username !== username)
                return user;
            updated = { ...user, role: nextRole, updatedAt: new Date().toISOString() };
            return updated;
        });
        if (!updated)
            throw new Error(`User not found: ${username}`);
        await this.writeUsers({ users });
        return updated;
    }
    async login(username, password) {
        validateUsername(username);
        const user = await this.findUserByUsername(username);
        if (!user)
            throw new AuthError(401, "INVALID_CREDENTIALS", "Invalid username or password.");
        if (user.disabled)
            throw new AuthError(403, "USER_DISABLED", "User account is disabled.");
        if (user.password !== password) {
            throw new AuthError(401, "INVALID_CREDENTIALS", "Invalid username or password.");
        }
        return { user, cookie: this.createSessionCookie(user.id) };
    }
    async changePassword(userId, currentPassword, newPassword) {
        validatePassword(newPassword);
        const file = await this.readUsers();
        const existing = file.users.find((user) => user.id === userId);
        if (!existing)
            throw new AuthError(401, "UNAUTHENTICATED", "Authentication required.");
        if (existing.password !== currentPassword) {
            throw new AuthError(401, "INVALID_CREDENTIALS", "Invalid current password.");
        }
        const updated = {
            ...existing,
            password: newPassword,
            passwordResetRequired: false,
            updatedAt: new Date().toISOString(),
        };
        await this.writeUsers({ users: file.users.map((user) => user.id === userId ? updated : user) });
        return updated;
    }
    async authenticate(cookieHeader) {
        const token = parseCookie(cookieHeader ?? "")[SESSION_COOKIE_NAME];
        if (!token)
            throw new AuthError(401, "UNAUTHENTICATED", "Authentication required.");
        const session = this.verifySessionToken(token);
        if (!session || session.expiresAt <= Date.now()) {
            throw new AuthError(401, "UNAUTHENTICATED", "Authentication required.");
        }
        const user = await this.findUserById(session.userId);
        if (!user)
            throw new AuthError(401, "UNAUTHENTICATED", "Authentication required.");
        if (user.disabled)
            throw new AuthError(403, "USER_DISABLED", "User account is disabled.");
        return user;
    }
    createLogoutCookie() {
        return `${SESSION_COOKIE_NAME}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax${this.secureCookie ? "; Secure" : ""}`;
    }
    toPublicUser(user) {
        return {
            id: user.id,
            username: user.username,
            role: user.role,
            passwordResetRequired: user.passwordResetRequired,
        };
    }
    async findUserByUsername(username) {
        return (await this.readUsers()).users.find((user) => user.username === username);
    }
    async findUserById(id) {
        return (await this.readUsers()).users.find((user) => user.id === id);
    }
    createSessionCookie(userId) {
        const now = Date.now();
        const session = {
            userId,
            issuedAt: now,
            expiresAt: now + SESSION_TTL_MS,
        };
        const token = this.signSession(session);
        const maxAge = Math.floor(SESSION_TTL_MS / 1000);
        return `${SESSION_COOKIE_NAME}=${token}; Path=/; Max-Age=${maxAge}; HttpOnly; SameSite=Lax${this.secureCookie ? "; Secure" : ""}`;
    }
    signSession(session) {
        const payload = Buffer.from(JSON.stringify(session), "utf-8").toString("base64url");
        const sig = createHmac("sha256", this.sessionSecret).update(payload).digest("base64url");
        return `${payload}.${sig}`;
    }
    verifySessionToken(token) {
        const [payload, sig] = token.split(".");
        if (!payload || !sig)
            return null;
        const expected = createHmac("sha256", this.sessionSecret).update(payload).digest("base64url");
        const left = Buffer.from(sig);
        const right = Buffer.from(expected);
        if (left.length !== right.length || !timingSafeEqual(left, right))
            return null;
        try {
            return JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
        }
        catch {
            return null;
        }
    }
    async readUsers() {
        try {
            const raw = await readFile(this.usersPath, "utf-8");
            const parsed = JSON.parse(raw);
            return { users: Array.isArray(parsed.users) ? parsed.users : [] };
        }
        catch {
            return { users: [] };
        }
    }
    async writeUsers(file) {
        await mkdir(dirname(this.usersPath), { recursive: true });
        await writeFile(this.usersPath, JSON.stringify(file, null, 2), "utf-8");
    }
}
export function validateUsername(username) {
    if (!USERNAME_PATTERN.test(username)) {
        throw new Error("Username must be 3-32 chars and only contain lowercase letters, numbers, _ or -.");
    }
}
export function validatePassword(password) {
    if (typeof password !== "string" || password.length < 8) {
        throw new Error("Password must be at least 8 characters.");
    }
}
export function validateStudioUserRole(role) {
    if (role === "user" || role === "admin")
        return role;
    throw new Error("Role must be user or admin.");
}
function parseCookie(header) {
    const cookies = {};
    for (const part of header.split(";")) {
        const [rawName, ...rawValue] = part.trim().split("=");
        if (!rawName)
            continue;
        cookies[rawName] = rawValue.join("=");
    }
    return cookies;
}
//# sourceMappingURL=auth.js.map