import { InvalidSessionError, RateLimitedError, UserNotFoundError } from "../index.js";
import { and, count, eq, gt, isNotNull, isNull, like, not, or, sql } from "drizzle-orm";
import { generateOpaqueToken, hashToken } from "@platform-modules/util/tokens";
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { timingSafeEqual } from "@platform-modules/util/crypto";
//#region src/engine-custom/password.ts
const ALGO = "SHA-512";
const ITERATIONS = 1e5;
const KEY_BYTES = 64;
const SALT_BYTES = 16;
const HMAC_BYTES = 32;
const SCHEME = "pbkdf2sha512-pep";
function asBufferSource(bytes) {
	return bytes;
}
function toB64(bytes) {
	const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
	return btoa(String.fromCharCode(...arr));
}
function fromB64(b64) {
	return new Uint8Array(atob(b64).split("").map((c) => c.charCodeAt(0)));
}
async function pbkdf2Derive(plain, salt, iterations) {
	const keyMaterial = await crypto.subtle.importKey("raw", new TextEncoder().encode(plain), "PBKDF2", false, ["deriveBits"]);
	return crypto.subtle.deriveBits({
		name: "PBKDF2",
		salt: asBufferSource(salt),
		iterations,
		hash: ALGO
	}, keyMaterial, 512);
}
/** P1a — after-KDF pepper: HMAC-SHA256(pbkdf2_digest, pepper). */
async function hmacPepper(derivedBits, pepper) {
	const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(pepper), {
		name: "HMAC",
		hash: "SHA-256"
	}, false, ["sign"]);
	return toB64(await crypto.subtle.sign("HMAC", key, derivedBits));
}
/**
* Returns `pbkdf2sha512-pep$<pepperVer>$<iter>$<saltB64>$<hmacB64>`.
* Pepper is applied AFTER PBKDF2 — never pbkdf2(HMAC(pw, pepper)).
*/
async function hashPassword(plain, pepper, opts) {
	const derive = opts?.deriveBits ?? pbkdf2Derive;
	const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
	const derived = await derive(plain, salt, ITERATIONS, ALGO, KEY_BYTES);
	const pepperSecret = pepper.secrets[pepper.currentVersion];
	if (!pepperSecret) throw new Error(`pepper version not configured: ${pepper.currentVersion}`);
	const hmacB64 = await hmacPepper(derived, pepperSecret);
	return `${SCHEME}$${pepper.currentVersion}$${ITERATIONS}$${toB64(salt)}$${hmacB64}`;
}
/**
* Builds a well-formed stored-hash string that `verifyPassword` evaluates in FULL (canonical
* `ITERATIONS`, correct salt + hmac byte-lengths, the active pepper version) yet which no real
* password can match. `signIn` runs it on the unknown/disabled-account branch so login latency is
* constant-time regardless of whether the email exists — closing the account-enumeration timing
* oracle. Derived from the module's own `ITERATIONS`, so the sentinel can NEVER drift from the
* canonical KDF cost (e.g. a future P1b iteration bump): iteration-parity is structural, not copied.
*/
function buildVerificationSentinel(pepperVersion) {
	const salt = toB64(new Uint8Array(SALT_BYTES));
	const hmac = toB64(new Uint8Array(HMAC_BYTES));
	return `${SCHEME}$${pepperVersion}$${ITERATIONS}$${salt}$${hmac}`;
}
/**
* Returns true when the stored hash should be re-hashed on next successful login:
* the pepper version is stale OR the stored work factor is below canonical ITERATIONS —
* but never when the stored factor exceeds ITERATIONS (never silently downgrade a hash
* imported at higher cost — raise ITERATIONS instead).
*
* Pepper rotation runbook:
*   1. Add new version to secrets; set currentVersion to it.
*   2. Monitor via countStalePasswordHashes until 0 old-version hashes remain.
*   3. Only then remove the old version from secrets.
* Skipping step 2 creates a timing gap: old-version hashes hit the early-return in verifyPassword
* while the sentinel runs the full KDF, leaking which emails have un-migrated accounts.
*/
function shouldUpgradeHash(stored, currentPepperVersion) {
	const parts = stored.split("$");
	if (parts.length !== 5 || parts[0] !== "pbkdf2sha512-pep") return false;
	const storedVer = parts[1];
	const storedIter = Number(parts[2]);
	if (!Number.isInteger(storedIter) || storedIter > ITERATIONS) return false;
	return storedVer !== currentPepperVersion || storedIter < ITERATIONS;
}
/** Parses iter, salt, and pepper version from `stored` — never from module constants alone. */
async function verifyPassword(plain, stored, pepper, opts) {
	const derive = opts?.deriveBits ?? pbkdf2Derive;
	const parts = stored.split("$");
	if (parts.length !== 5 || parts[0] !== "pbkdf2sha512-pep") return false;
	const [, pepperVer, iterStr, saltB64, expectedHmacB64] = parts;
	const pepperSecret = pepper.secrets[pepperVer];
	if (!pepperSecret) return false;
	const iterations = Number(iterStr);
	if (!Number.isInteger(iterations) || iterations <= 0) return false;
	let salt;
	try {
		salt = fromB64(saltB64);
	} catch {
		return false;
	}
	const hmacB64 = await hmacPepper(await derive(plain, salt, iterations, ALGO, KEY_BYTES), pepperSecret);
	return timingSafeEqual(hmacB64, expectedHmacB64);
}
//#endregion
//#region src/engine-custom/schema.ts
/** Engine-owned identity row — `sessionVersion` drives `sv` revocation. */
const authUsers = pgTable("auth_users", {
	id: text("id").primaryKey(),
	email: text("email").notNull().unique(),
	passwordHash: text("password_hash"),
	sessionVersion: integer("session_version").notNull().default(0),
	roles: text("roles").notNull().default("[\"user\"]"),
	status: text("status").notNull().default("active"),
	createdAt: timestamp("created_at", { withTimezone: true }).notNull()
});
/** Idempotent DDL to create the auth_users table (neon-http: one statement). FK root — run first. */
const authUsersCreationSql = () => `CREATE TABLE IF NOT EXISTS auth_users (
    id text PRIMARY KEY,
    email text NOT NULL UNIQUE,
    password_hash text,
    session_version integer NOT NULL DEFAULT 0,
    roles text NOT NULL DEFAULT '["user"]',
    status text NOT NULL DEFAULT 'active',
    created_at timestamptz NOT NULL
  )`;
/** Idempotent DDL to create the user_sessions table (neon-http: one statement). FKs auth_users. */
const userSessionsCreationSql = () => `CREATE TABLE IF NOT EXISTS user_sessions (
    id text PRIMARY KEY,
    user_id text NOT NULL REFERENCES auth_users(id),
    refresh_token_hash text NOT NULL,
    previous_refresh_token_hash text,
    last_refreshed_at timestamptz,
    expires_at timestamptz NOT NULL,
    status text NOT NULL DEFAULT 'active'
  )`;
/** Idempotent DDL for adopters migrating existing auth_users tables (neon-http: one statement). */
const authUsersStatusMigrationSql = () => `ALTER TABLE auth_users ADD COLUMN IF NOT EXISTS status text NOT NULL DEFAULT 'active'`;
/** Idempotent DDL to create the service_tokens table (neon-http: one statement). FKs auth_users. */
const serviceTokensCreationSql = () => `CREATE TABLE IF NOT EXISTS service_tokens (
    id text PRIMARY KEY,
    user_id text NOT NULL REFERENCES auth_users(id),
    token_hash text NOT NULL UNIQUE,
    label text NOT NULL,
    expires_at timestamptz,
    status text NOT NULL DEFAULT 'active',
    created_at timestamptz NOT NULL,
    last_used_at timestamptz
  )`;
/** One refresh-token row per device/session — no access-token hash column. */
const userSessions = pgTable("user_sessions", {
	id: text("id").primaryKey(),
	userId: text("user_id").notNull().references(() => authUsers.id),
	refreshTokenHash: text("refresh_token_hash").notNull(),
	previousRefreshTokenHash: text("previous_refresh_token_hash"),
	lastRefreshedAt: timestamp("last_refreshed_at", { withTimezone: true }),
	expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
	status: text("status").notNull().default("active")
});
/** Opaque machine/agent credential (PAT shape) — independent of userSessions; not rotating. */
const serviceTokens = pgTable("service_tokens", {
	id: text("id").primaryKey(),
	userId: text("user_id").notNull().references(() => authUsers.id),
	tokenHash: text("token_hash").notNull().unique(),
	label: text("label").notNull(),
	expiresAt: timestamp("expires_at", { withTimezone: true }),
	status: text("status").notNull().default("active"),
	createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
	lastUsedAt: timestamp("last_used_at", { withTimezone: true })
});
const customAuthSchema = {
	authUsers,
	userSessions,
	serviceTokens
};
//#endregion
//#region src/engine-custom/session.ts
/**
* Raw Web-Crypto HS256 session tokens.
* Recomputes HMAC over received `header.payload`; never trusts header `alg`.
*/
const ALG = {
	name: "HMAC",
	hash: "SHA-256"
};
const SKEW_SECS = 30;
const DEFAULT_ACCESS_TTL_SECS = 900;
const enc = new TextEncoder();
const _hmacKeyCache = /* @__PURE__ */ new Map();
function bufToBase64Url(buf) {
	const bytes = new Uint8Array(buf);
	let bin = "";
	for (const b of bytes) bin += String.fromCharCode(b);
	return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function base64UrlToBytes(s) {
	const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
	const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + pad;
	const binary = atob(b64);
	const out = new Uint8Array(binary.length);
	for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
	return out;
}
function jsonToBase64Url(obj) {
	return bufToBase64Url(enc.encode(JSON.stringify(obj)).buffer);
}
async function importHmacKey(secret) {
	const cached = _hmacKeyCache.get(secret);
	if (cached) return cached;
	const key = await crypto.subtle.importKey("raw", enc.encode(secret), ALG, false, ["sign", "verify"]);
	_hmacKeyCache.set(secret, key);
	return key;
}
const HEADER_B64 = jsonToBase64Url({
	alg: "HS256",
	typ: "JWT"
});
async function signSession(claims, secret, ttlSec = DEFAULT_ACCESS_TTL_SECS) {
	const now = Math.floor(Date.now() / 1e3);
	const payload = {
		...claims,
		iat: now,
		exp: now + ttlSec
	};
	const headerPayload = `${HEADER_B64}.${jsonToBase64Url(payload)}`;
	const key = await importHmacKey(secret);
	return `${headerPayload}.${bufToBase64Url(await crypto.subtle.sign(ALG, key, enc.encode(headerPayload)))}`;
}
/** Verify against one or more secrets (rotation). Returns null on any failure — never throws. */
async function verifySession(jwt, secrets, opts) {
	try {
		const parts = jwt.split(".");
		if (parts.length !== 3) return null;
		const [headerB64, payloadB64, sigB64] = parts;
		const sigBytes = base64UrlToBytes(sigB64);
		const headerPayload = enc.encode(`${headerB64}.${payloadB64}`);
		const secretList = Array.isArray(secrets) ? secrets : [secrets];
		let verified = false;
		for (const secret of secretList) {
			const key = await importHmacKey(secret);
			if (await crypto.subtle.verify(ALG, key, sigBytes.buffer, headerPayload)) {
				verified = true;
				break;
			}
		}
		if (!verified) return null;
		const raw = JSON.parse(new TextDecoder().decode(base64UrlToBytes(payloadB64)));
		const now = Math.floor(Date.now() / 1e3);
		if (typeof raw.exp !== "number" || now > raw.exp + SKEW_SECS) return null;
		if (typeof raw.iat !== "number" || raw.iat > now + SKEW_SECS) return null;
		if (typeof raw.sub !== "string" || typeof raw.sid !== "string" || typeof raw.sv !== "number") return null;
		if (opts?.minSessionVersion !== void 0 && raw.sv < opts.minSessionVersion) return null;
		return {
			sub: raw.sub,
			sid: raw.sid,
			sv: raw.sv,
			iat: raw.iat,
			exp: raw.exp
		};
	} catch {
		return null;
	}
}
const ACCESS_TOKEN_TTL_SECS = DEFAULT_ACCESS_TTL_SECS;
const REFRESH_TOKEN_TTL_SECS = 2592e3;
//#endregion
//#region src/engine-custom/refresh.ts
const SESSION_TTL_MS = 2592e6;
const GRACE_WINDOW_MS = 6e4;
function parseRoles$2(raw) {
	try {
		const parsed = JSON.parse(raw);
		return Array.isArray(parsed) ? parsed : ["user"];
	} catch {
		return ["user"];
	}
}
async function mintAccessToken(user, jwtSecrets) {
	return signSession({
		sub: user.userId,
		sid: user.sessionId,
		sv: user.sessionVersion
	}, jwtSecrets[0], 900);
}
async function rotateRefreshToken(db, tables, rawRefreshToken, jwtSecrets) {
	const oldHash = await hashToken(rawRefreshToken);
	const now = /* @__PURE__ */ new Date();
	const [row] = await db.select({
		sessionId: tables.userSessions.id,
		userId: tables.userSessions.userId,
		sessionVersion: tables.authUsers.sessionVersion,
		roles: tables.authUsers.roles,
		status: tables.userSessions.status
	}).from(tables.userSessions).innerJoin(tables.authUsers, eq(tables.userSessions.userId, tables.authUsers.id)).where(and(eq(tables.userSessions.refreshTokenHash, oldHash), eq(tables.userSessions.status, "active"), gt(tables.userSessions.expiresAt, now))).limit(1);
	if (!row) {
		const graceCutoff = /* @__PURE__ */ new Date(now.getTime() - GRACE_WINDOW_MS);
		const [graceRow] = await db.select({
			sessionId: tables.userSessions.id,
			userId: tables.userSessions.userId,
			sessionVersion: tables.authUsers.sessionVersion,
			roles: tables.authUsers.roles
		}).from(tables.userSessions).innerJoin(tables.authUsers, eq(tables.userSessions.userId, tables.authUsers.id)).where(and(eq(tables.userSessions.previousRefreshTokenHash, oldHash), gt(tables.userSessions.lastRefreshedAt, graceCutoff), eq(tables.userSessions.status, "active"), gt(tables.userSessions.expiresAt, now))).limit(1);
		if (graceRow) {
			const user = {
				sessionId: graceRow.sessionId,
				userId: graceRow.userId,
				sessionVersion: graceRow.sessionVersion,
				roles: parseRoles$2(graceRow.roles)
			};
			return {
				kind: "grace",
				user,
				accessToken: await mintAccessToken(user, jwtSecrets)
			};
		}
		const [reuseRow] = await db.select({
			sessionId: tables.userSessions.id,
			userId: tables.userSessions.userId
		}).from(tables.userSessions).where(eq(tables.userSessions.previousRefreshTokenHash, oldHash)).limit(1);
		if (reuseRow) {
			await db.update(tables.userSessions).set({ status: "revoked" }).where(eq(tables.userSessions.id, reuseRow.sessionId));
			const [bumped] = await db.update(tables.authUsers).set({ sessionVersion: sql`${tables.authUsers.sessionVersion} + 1` }).where(eq(tables.authUsers.id, reuseRow.userId)).returning({ sessionVersion: tables.authUsers.sessionVersion });
			return {
				kind: "reuse_detected",
				userId: reuseRow.userId,
				sessionId: reuseRow.sessionId,
				newSessionVersion: bumped?.sessionVersion ?? 0
			};
		}
		return { kind: "invalid" };
	}
	const user = {
		sessionId: row.sessionId,
		userId: row.userId,
		sessionVersion: row.sessionVersion,
		roles: parseRoles$2(row.roles)
	};
	const newRefreshToken = generateOpaqueToken();
	const newHash = await hashToken(newRefreshToken);
	const updated = await db.update(tables.userSessions).set({
		refreshTokenHash: newHash,
		previousRefreshTokenHash: oldHash,
		lastRefreshedAt: now,
		expiresAt: new Date(now.getTime() + SESSION_TTL_MS)
	}).where(eq(tables.userSessions.refreshTokenHash, oldHash)).returning({ id: tables.userSessions.id });
	const accessToken = await mintAccessToken(user, jwtSecrets);
	if (updated.length === 0) return {
		kind: "race_lost",
		user,
		accessToken
	};
	return {
		kind: "rotated",
		user,
		accessToken,
		refreshToken: newRefreshToken
	};
}
async function bumpSessionVersion(db, usersTable, userId) {
	const [row] = await db.update(usersTable).set({ sessionVersion: sql`${usersTable.sessionVersion} + 1` }).where(eq(usersTable.id, userId)).returning({ sessionVersion: usersTable.sessionVersion });
	if (!row) throw new Error(`bumpSessionVersion: user not found (${userId})`);
	return row.sessionVersion;
}
//#endregion
//#region src/engine-custom/service-token.ts
const LAST_USED_THROTTLE_MS = 36e5;
function parseRoles$1(raw) {
	try {
		const parsed = JSON.parse(raw);
		return Array.isArray(parsed) ? parsed : ["user"];
	} catch {
		return ["user"];
	}
}
async function issueServiceToken(db, tables, input) {
	const now = input.now ?? /* @__PURE__ */ new Date();
	const id = crypto.randomUUID();
	const token = generateOpaqueToken();
	const tokenHash = await hashToken(token);
	const expiresAt = input.ttlMs != null ? new Date(now.getTime() + input.ttlMs) : null;
	await db.insert(tables.serviceTokens).values({
		id,
		userId: input.userId,
		tokenHash,
		label: input.label,
		expiresAt,
		status: "active",
		createdAt: now,
		lastUsedAt: null
	});
	return {
		id,
		token
	};
}
async function resolveServiceToken(db, tables, rawToken, opts) {
	const now = opts?.now ?? /* @__PURE__ */ new Date();
	const tokenHash = await hashToken(rawToken);
	const [row] = await db.select({
		id: tables.serviceTokens.id,
		userId: tables.serviceTokens.userId,
		lastUsedAt: tables.serviceTokens.lastUsedAt,
		roles: tables.authUsers.roles
	}).from(tables.serviceTokens).innerJoin(tables.authUsers, eq(tables.serviceTokens.userId, tables.authUsers.id)).where(and(eq(tables.serviceTokens.tokenHash, tokenHash), eq(tables.serviceTokens.status, "active"), eq(tables.authUsers.status, "active"), or(isNull(tables.serviceTokens.expiresAt), gt(tables.serviceTokens.expiresAt, now)))).limit(1);
	if (!row) return null;
	const last = row.lastUsedAt ? new Date(row.lastUsedAt).getTime() : 0;
	if (now.getTime() - last >= LAST_USED_THROTTLE_MS) try {
		await db.update(tables.serviceTokens).set({ lastUsedAt: now }).where(eq(tables.serviceTokens.id, row.id));
	} catch {}
	return {
		userId: row.userId,
		sessionId: "svc:" + row.id,
		roles: parseRoles$1(row.roles)
	};
}
async function listServiceTokensForUser(db, tables, userId) {
	return db.select({
		id: tables.serviceTokens.id,
		label: tables.serviceTokens.label,
		createdAt: tables.serviceTokens.createdAt,
		expiresAt: tables.serviceTokens.expiresAt,
		lastUsedAt: tables.serviceTokens.lastUsedAt,
		status: tables.serviceTokens.status
	}).from(tables.serviceTokens).where(and(eq(tables.serviceTokens.userId, userId), eq(tables.serviceTokens.status, "active")));
}
async function revokeServiceToken(db, serviceTokensTable, id) {
	await db.update(serviceTokensTable).set({ status: "revoked" }).where(eq(serviceTokensTable.id, id));
}
async function revokeServiceTokensForUser(db, serviceTokensTable, userId) {
	await db.update(serviceTokensTable).set({ status: "revoked" }).where(eq(serviceTokensTable.userId, userId));
}
//#endregion
//#region src/engine-custom/index.ts
var InvalidEngineConfigError = class extends Error {
	field;
	name = "InvalidEngineConfigError";
	constructor(message, field) {
		super(message);
		this.field = field;
	}
};
function parseRoles(raw) {
	try {
		const parsed = JSON.parse(raw);
		return Array.isArray(parsed) ? parsed : ["user"];
	} catch {
		return ["user"];
	}
}
function toPrincipal(userId, sessionId, roles) {
	return {
		userId,
		sessionId,
		roles
	};
}
async function lookupUserSessionState(db, userId, usersTable = authUsers) {
	const [row] = await db.select({
		sessionVersion: usersTable.sessionVersion,
		roles: usersTable.roles,
		status: usersTable.status
	}).from(usersTable).where(eq(usersTable.id, userId)).limit(1);
	if (!row) return null;
	return {
		sessionVersion: row.sessionVersion,
		roles: parseRoles(row.roles),
		status: row.status === "disabled" ? "disabled" : "active"
	};
}
async function assertUserExists(db, usersTable, userId) {
	const [row] = await db.select({ id: usersTable.id }).from(usersTable).where(eq(usersTable.id, userId)).limit(1);
	if (!row) throw new UserNotFoundError(`user not found: ${userId}`, userId);
}
function createCustomEngine(config) {
	const tables = {
		authUsers: config.schema?.authUsers ?? authUsers,
		userSessions: config.schema?.userSessions ?? userSessions,
		serviceTokens: config.schema?.serviceTokens ?? serviceTokens
	};
	if (config.jwtSecrets.length === 0 || config.jwtSecrets.some((s) => s.length === 0)) throw new InvalidEngineConfigError("createCustomEngine: jwtSecrets must contain at least one non-empty secret", "jwtSecrets");
	if (!config.pepper.secrets[config.pepper.currentVersion]) throw new InvalidEngineConfigError(`createCustomEngine: pepper secret missing for currentVersion "${config.pepper.currentVersion}"`, "pepper");
	const jwtSecrets = config.jwtSecrets;
	return {
		async signIn(credentials) {
			if (config.rateLimit) {
				if (!(await config.rateLimit("auth.login", credentials.email.toLowerCase())).allowed) throw new RateLimitedError("rate limited", "auth.login");
			}
			const [user] = await config.db.select().from(tables.authUsers).where(eq(tables.authUsers.email, credentials.email.toLowerCase())).limit(1);
			if (!user?.passwordHash || user.status === "disabled") {
				const sentinel = buildVerificationSentinel(config.pepper.currentVersion);
				await verifyPassword(credentials.password, sentinel, config.pepper);
				throw new InvalidSessionError("invalid credentials", "bad_credentials");
			}
			if (!await verifyPassword(credentials.password, user.passwordHash, config.pepper)) throw new InvalidSessionError("invalid credentials", "bad_credentials");
			try {
				if (shouldUpgradeHash(user.passwordHash, config.pepper.currentVersion)) {
					const upgradedHash = await hashPassword(credentials.password, config.pepper);
					await config.db.update(tables.authUsers).set({ passwordHash: upgradedHash }).where(and(eq(tables.authUsers.id, user.id), eq(tables.authUsers.passwordHash, user.passwordHash)));
				}
			} catch {}
			const sessionId = crypto.randomUUID();
			const refreshToken = generateOpaqueToken();
			const refreshTokenHash = await hashToken(refreshToken);
			const expiresAt = new Date(Date.now() + SESSION_TTL_MS);
			const roles = parseRoles(user.roles);
			await config.db.insert(tables.userSessions).values({
				id: sessionId,
				userId: user.id,
				refreshTokenHash,
				expiresAt,
				status: "active"
			});
			const accessToken = await signSession({
				sub: user.id,
				sid: sessionId,
				sv: user.sessionVersion
			}, jwtSecrets[0], 900);
			return {
				principal: toPrincipal(user.id, sessionId, roles),
				accessToken,
				refreshToken
			};
		},
		async signOut(sessionId) {
			await config.db.update(tables.userSessions).set({ status: "revoked" }).where(eq(tables.userSessions.id, sessionId));
		},
		async verifySession(token) {
			const claims = await verifySession(token, jwtSecrets);
			if (!claims) return null;
			const [row] = await config.db.select({
				sessionVersion: tables.authUsers.sessionVersion,
				roles: tables.authUsers.roles,
				userStatus: tables.authUsers.status,
				sessionStatus: tables.userSessions.status
			}).from(tables.userSessions).innerJoin(tables.authUsers, eq(tables.userSessions.userId, tables.authUsers.id)).where(and(eq(tables.userSessions.id, claims.sid), eq(tables.authUsers.id, claims.sub), gt(tables.userSessions.expiresAt, /* @__PURE__ */ new Date()))).limit(1);
			if (!row) return null;
			if (claims.sv < row.sessionVersion || row.userStatus !== "active" || row.sessionStatus !== "active") return null;
			return toPrincipal(claims.sub, claims.sid, parseRoles(row.roles));
		},
		async refresh(refreshToken) {
			if (config.rateLimit) {
				if (!(await config.rateLimit("auth.refresh", (await hashToken(refreshToken)).slice(0, 16))).allowed) throw new RateLimitedError("rate limited", "auth.refresh");
			}
			const result = await rotateRefreshToken(config.db, tables, refreshToken, jwtSecrets);
			if (result.kind === "invalid") return null;
			if (result.kind === "reuse_detected") return null;
			if (result.kind === "grace" || result.kind === "race_lost") return {
				principal: toPrincipal(result.user.userId, result.user.sessionId, result.user.roles),
				accessToken: result.accessToken
			};
			return {
				principal: toPrincipal(result.user.userId, result.user.sessionId, result.user.roles),
				accessToken: result.accessToken,
				refreshToken: result.refreshToken
			};
		},
		async createUser(input) {
			const userId = crypto.randomUUID();
			const passwordHash = await hashPassword(input.password, config.pepper);
			const roles = input.roles ?? ["user"];
			await config.db.insert(tables.authUsers).values({
				id: userId,
				email: input.email.toLowerCase(),
				passwordHash,
				sessionVersion: 0,
				roles: JSON.stringify(roles),
				status: "active",
				createdAt: /* @__PURE__ */ new Date()
			});
			return { userId };
		},
		async setPassword(userId, password) {
			const passwordHash = await hashPassword(password, config.pepper);
			await config.db.transaction(async (tx) => {
				await tx.update(tables.authUsers).set({ passwordHash }).where(eq(tables.authUsers.id, userId));
				await tx.update(tables.userSessions).set({ status: "revoked" }).where(eq(tables.userSessions.userId, userId));
				await bumpSessionVersion(tx, tables.authUsers, userId);
			});
		},
		async verifyPassword(password, storedHash) {
			return verifyPassword(password, storedHash, config.pepper);
		},
		async listUsers(opts) {
			const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 200);
			const offset = Math.max(opts?.offset ?? 0, 0);
			const [totalRow] = await config.db.select({ total: count() }).from(tables.authUsers);
			return {
				users: (await config.db.select({
					id: tables.authUsers.id,
					email: tables.authUsers.email,
					roles: tables.authUsers.roles,
					status: tables.authUsers.status,
					createdAt: tables.authUsers.createdAt
				}).from(tables.authUsers).orderBy(tables.authUsers.createdAt, tables.authUsers.id).limit(limit).offset(offset)).map((row) => ({
					id: row.id,
					email: row.email,
					roles: parseRoles(row.roles),
					status: row.status === "disabled" ? "disabled" : "active",
					createdAt: row.createdAt.toISOString()
				})),
				total: Number(totalRow?.total ?? 0)
			};
		},
		async setUserRoles(userId, roles) {
			await assertUserExists(config.db, tables.authUsers, userId);
			await config.db.update(tables.authUsers).set({ roles: JSON.stringify(roles) }).where(eq(tables.authUsers.id, userId));
			await bumpSessionVersion(config.db, tables.authUsers, userId);
		},
		async disableUser(userId) {
			await assertUserExists(config.db, tables.authUsers, userId);
			await config.db.update(tables.authUsers).set({ status: "disabled" }).where(eq(tables.authUsers.id, userId));
			await config.db.update(tables.userSessions).set({ status: "revoked" }).where(eq(tables.userSessions.userId, userId));
			await revokeServiceTokensForUser(config.db, tables.serviceTokens, userId);
			await bumpSessionVersion(config.db, tables.authUsers, userId);
		}
	};
}
/**
* Returns the count of password hashes NOT on the current pepper version.
* Zero means every active hash has been lazily upgraded — safe to drop the old version
* from `PepperBinding.secrets` without opening a timing gap. See `shouldUpgradeHash` runbook.
*/
async function countStalePasswordHashes(db, currentPepperVersion, usersTable = authUsers) {
	if (!/^[a-zA-Z0-9.-]+$/.test(currentPepperVersion)) throw new Error(`invalid pepper version: ${currentPepperVersion}`);
	const prefix = `${SCHEME}$${currentPepperVersion}$`;
	const [row] = await db.select({ n: count() }).from(usersTable).where(and(isNotNull(usersTable.passwordHash), not(like(usersTable.passwordHash, `${prefix}%`))));
	return Number(row?.n ?? 0);
}
//#endregion
export { ACCESS_TOKEN_TTL_SECS, InvalidEngineConfigError, REFRESH_TOKEN_TTL_SECS, authUsers, authUsersCreationSql, authUsersStatusMigrationSql, bumpSessionVersion, countStalePasswordHashes, createCustomEngine, customAuthSchema, hashPassword, issueServiceToken, listServiceTokensForUser, lookupUserSessionState, resolveServiceToken, revokeServiceToken, revokeServiceTokensForUser, rotateRefreshToken, serviceTokens, serviceTokensCreationSql, shouldUpgradeHash, signSession, userSessions, userSessionsCreationSql, verifySession as verifyAccessToken, verifyPassword };
