import * as jose from 'jose';
/**
 * Thrown when a JWT verifies cryptographically but its decoded payload
 * doesn't match the expected `SessionPayload` shape. We map this to a 401
 * (not 500) because it represents a forged-but-correctly-signed-by-the-CMS
 * token rather than a server bug.
 */
export class InvalidSessionPayloadError extends Error {
    constructor(reason) {
        super(`Session payload is malformed: ${reason}`);
        this.name = 'InvalidSessionPayloadError';
    }
}
/**
 * Narrow an arbitrary JWT payload to a `SessionPayload`. Required because
 * `jose.jwtVerify()` returns `JWTPayload`, which is `Record<string, unknown>`
 * — without an explicit shape check, downstream code reading `payload.role`
 * would happily evaluate `undefined` against the `WRITE_ROLES`/`ADMIN_ROLES`
 * sets and grant `false` (deny), which is safe — but reading `payload.userId`
 * to look up the user would crash deep in the request, after side effects.
 *
 * Centralizing the check keeps the failure mode crisp: the request returns
 * 401 and the audit log shows "malformed session" instead of a 500.
 */
function assertSessionPayload(payload) {
    if (typeof payload !== 'object' || payload === null) {
        throw new InvalidSessionPayloadError('payload is not an object');
    }
    const p = payload;
    if (typeof p.userId !== 'string' || p.userId.length === 0) {
        throw new InvalidSessionPayloadError('missing or invalid `userId`');
    }
    if (typeof p.role !== 'string' || p.role.length === 0) {
        throw new InvalidSessionPayloadError('missing or invalid `role`');
    }
    if (typeof p.sessionId !== 'string' || p.sessionId.length === 0) {
        throw new InvalidSessionPayloadError('missing or invalid `sessionId`');
    }
    if (p.fingerprint !== undefined && typeof p.fingerprint !== 'string') {
        throw new InvalidSessionPayloadError('`fingerprint`, when present, must be a string');
    }
}
const DEFAULT_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
/** Create a signed JWT session token. */
export async function createSession(payload, options) {
    const secret = new TextEncoder().encode(options.secret);
    return new jose.SignJWT({ ...payload })
        .setProtectedHeader({ alg: 'HS256' })
        .setIssuedAt()
        .setExpirationTime(`${options.maxAge ?? DEFAULT_MAX_AGE}s`)
        .setIssuer(options.issuer ?? 'actuate-cms')
        .setAudience(options.audience ?? 'actuate-cms')
        .sign(secret);
}
/** Verify and decode a JWT session token. */
export async function verifySession(token, options) {
    const secret = new TextEncoder().encode(options.secret);
    const { payload } = await jose.jwtVerify(token, secret, {
        issuer: options.issuer ?? 'actuate-cms',
        audience: options.audience ?? 'actuate-cms',
    });
    assertSessionPayload(payload);
    // Strip standard JWT claims (iat/exp/iss/aud/etc) so the returned object
    // is ONLY the SessionPayload fields we explicitly validated. This prevents
    // callers (e.g. `refreshSession`) from accidentally re-signing arbitrary
    // attacker-supplied claims.
    const safe = {
        userId: payload.userId,
        role: payload.role,
        sessionId: payload.sessionId,
    };
    if (payload.fingerprint !== undefined)
        safe.fingerprint = payload.fingerprint;
    return safe;
}
/** Revoke a session by marking it in the database. */
export async function revokeSession(sessionId, db) {
    await db.session.update({
        where: { id: sessionId },
        data: { revokedAt: new Date() },
    });
}
/**
 * Refresh a session token, issuing a new JWT with an extended expiry.
 *
 * When `db` is supplied, the existing `Session` row is verified to be active
 * and its `expiresAt` is extended in lockstep with the new JWT. Without that
 * check, a revoked session that was still in a user's cookie could be
 * "un-revoked" simply by refreshing it.
 */
export async function refreshSession(token, options, db) {
    const payload = await verifySession(token, options);
    if (db) {
        const session = await db.session.findUnique({ where: { id: payload.sessionId } });
        if (!session || session.revokedAt || session.expiresAt < new Date()) {
            throw new Error('Session is no longer active');
        }
        const maxAge = options.maxAge ?? DEFAULT_MAX_AGE;
        await db.session.update({
            where: { id: payload.sessionId },
            data: { expiresAt: new Date(Date.now() + maxAge * 1000) },
        });
    }
    return createSession(payload, options);
}
//# sourceMappingURL=session.js.map