globalThis.process ??= {};
globalThis.process.env ??= {};
import { D as gt, E as eq, F as or, G as text, R as pgTable, T as and, Tt as sql, U as timestamp, Z as integer, j as isNull } from "./d1_DTqC4o_t.mjs";
import { r as timingSafeEqual, rt as count } from "./crypto_CY2zBtG3.mjs";
//#region ../../packages/auth/dist/index.js
var AuthError = class extends Error {};
var InvalidSessionError = class extends AuthError {
	reason;
	name = "InvalidSessionError";
	constructor(message, reason) {
		super(message);
		this.reason = reason;
	}
};
var RateLimitedError = class extends AuthError {
	scope;
	name = "RateLimitedError";
	constructor(message, scope) {
		super(message);
		this.scope = scope;
	}
};
var RoleDeniedError = class extends AuthError {
	role;
	principal;
	name = "RoleDeniedError";
	constructor(message, role, principal) {
		super(message);
		this.role = role;
		this.principal = principal;
	}
};
var UserNotFoundError = class extends AuthError {
	userId;
	name = "UserNotFoundError";
	constructor(message, userId) {
		super(message);
		this.userId = userId;
	}
};
var _cookieReCache = /* @__PURE__ */ new Map();
function cookieRe(name) {
	const cached = _cookieReCache.get(name);
	if (cached) return cached;
	const re = new RegExp(`(?:^|;\\s*)${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=([^;]+)`);
	_cookieReCache.set(name, re);
	return re;
}
async function getSession(headers, engine, opts) {
	try {
		const h = headers instanceof Headers ? headers : new Headers(headers);
		const cookieName = opts?.accessCookieName ?? "access_token";
		let token = null;
		const auth = h.get("authorization");
		if (auth?.startsWith("Bearer ")) token = auth.slice(7).trim() || null;
		if (!token) token = (h.get("cookie") ?? "").match(cookieRe(cookieName))?.[1]?.trim() ?? null;
		if (!token) return null;
		return await engine.verifySession(token);
	} catch {
		return null;
	}
}
function isAtLeastRole(principal, role, hierarchy) {
	const targetIdx = hierarchy.indexOf(role);
	if (targetIdx < 0) return false;
	for (const r of principal.roles) {
		const idx = hierarchy.indexOf(r);
		if (idx >= 0 && idx >= targetIdx) return true;
	}
	return false;
}
/**
* Role gate — the role-based twin of `requirePermission`. Returns a resolver:
*   - `principal == null`              → throws `InvalidSessionError`
*   - `!isAtLeastRole(p, role, hier)`  → throws `RoleDeniedError`
*   - else                             → returns the principal
*
* Use with engines that populate `Principal.roles` (the custom engine does; `requirePermission`
* is capability-based and inert there). Host admin guard:
* `requireRole('admin', HIERARCHY)(await getSession(headers, engine))`.
*/
function requireRole(role, hierarchy) {
	return (principal) => {
		if (!principal) throw new InvalidSessionError("no session", "missing_principal");
		if (!isAtLeastRole(principal, role, hierarchy)) throw new RoleDeniedError(`role denied: ${role}`, role, principal);
		return principal;
	};
}
//#endregion
//#region ../../packages/util/dist/tokens.js
/**
* `@platform-modules/util/tokens` — opaque CSPRNG tokens (store the hash, never the token).
*/
function bytesToBase64Url(bytes) {
	let binary = "";
	for (const b of bytes) binary += String.fromCharCode(b);
	return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
/** N cryptographically-random bytes (default 32), base64url-encoded (no padding). */
function generateOpaqueToken(bytes = 32) {
	return bytesToBase64Url(crypto.getRandomValues(new Uint8Array(bytes)));
}
/** SHA-256 hex digest of `token` — the value stored in the DB. */
async function hashToken(token) {
	const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token));
	return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
}
//#endregion
//#region ../../packages/auth/dist/engine-custom/index.js
var ALGO = "SHA-512";
var ITERATIONS = 1e5;
var KEY_BYTES = 64;
var SALT_BYTES = 16;
var HMAC_BYTES = 32;
var 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) {
	return `${SCHEME}$${pepperVersion}$${ITERATIONS}$${toB64(new Uint8Array(SALT_BYTES))}$${toB64(new Uint8Array(HMAC_BYTES))}`;
}
/**
* 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);
}
/** Engine-owned identity row — `sessionVersion` drives `sv` revocation. */
var 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()
});
/** One refresh-token row per device/session — no access-token hash column. */
var 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. */
var 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 })
});
var customAuthSchema = {
	authUsers,
	userSessions,
	serviceTokens
};
/**
* Raw Web-Crypto HS256 session tokens.
* Recomputes HMAC over received `header.payload`; never trusts header `alg`.
*/
var ALG = {
	name: "HMAC",
	hash: "SHA-256"
};
var SKEW_SECS = 30;
var DEFAULT_ACCESS_TTL_SECS = 900;
var enc = new TextEncoder();
var _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;
}
var 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 headerPayload = `${HEADER_B64}.${jsonToBase64Url({
		...claims,
		iat: now,
		exp: now + ttlSec
	})}`;
	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;
	}
}
var REFRESH_TOKEN_TTL_SECS = 2592e3;
var SESSION_TTL_MS = 2592e6;
var 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;
}
var 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));
}
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 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);
		}
	};
}
//#endregion
export { issueServiceToken as a, revokeServiceToken as c, getSession as d, requireRole as f, hashPassword as i, verifyPassword as l, createCustomEngine as n, listServiceTokensForUser as o, customAuthSchema as r, resolveServiceToken as s, REFRESH_TOKEN_TTL_SECS as t, InvalidSessionError as u };
