84 lines
2.2 KiB
TypeScript

import { SignJWT, jwtVerify, type JWTPayload } from "jose";
import { randomUUID } from "crypto";
const ALGORITHM = "HS256";
const PURPOSE = "rebreak.cooldown";
function getSecret(): Uint8Array {
const raw =
process.env.REBREAK_COOLDOWN_SECRET ||
process.env.NUXT_AUTH_SECRET ||
// Last-resort fallback — deterministic within a single process lifetime.
// A proper secret MUST be set via Infisical in production.
"rebreak-cooldown-insecure-fallback-replace-me";
return new TextEncoder().encode(raw);
}
export interface CooldownTokenPayload {
userId: string;
jti: string;
cooldownEndsAt: string; // ISO-8601
}
/**
* Signs a short-lived JWT that the iOS app can present to prove it has
* permission to disable the DNS protection (cooldown expired on the server).
*
* Lifetime: 5 minutes — short enough to prevent replay attacks.
* The `jti` ties the token to the exact CooldownRequest row.
*/
export async function signCooldownToken(
userId: string,
jti: string,
cooldownEndsAt: Date,
): Promise<string> {
const secret = getSecret();
const now = Math.floor(Date.now() / 1000);
return new SignJWT({
sub: userId,
jti,
purpose: PURPOSE,
cooldown_ends_at: cooldownEndsAt.toISOString(),
} satisfies JWTPayload & { purpose: string; cooldown_ends_at: string })
.setProtectedHeader({ alg: ALGORITHM })
.setIssuedAt(now)
.setExpirationTime(now + 5 * 60) // 5 minutes
.sign(secret);
}
/**
* Verifies the token and returns its payload or null if invalid/expired.
*/
export async function verifyCooldownToken(
token: string,
): Promise<CooldownTokenPayload | null> {
try {
const { payload } = await jwtVerify(token, getSecret(), {
algorithms: [ALGORITHM],
});
if (
typeof payload.sub !== "string" ||
typeof payload.jti !== "string" ||
payload.purpose !== PURPOSE ||
typeof payload.cooldown_ends_at !== "string"
) {
return null;
}
return {
userId: payload.sub,
jti: payload.jti,
cooldownEndsAt: payload.cooldown_ends_at,
};
} catch {
return null;
}
}
/** Convenience: generate a new JTI (UUID v4). */
export function generateJti(): string {
return randomUUID();
}