import crypto from 'node:crypto'; import { jwtVerify, SignJWT } from 'jose'; import type { AppConfig } from '../config.js'; import { unauthorized } from '../errors.js'; if (!globalThis.crypto?.subtle) { Object.assign(globalThis, { crypto: crypto.webcrypto, }); } if (typeof globalThis.structuredClone !== 'function') { Object.assign(globalThis, { structuredClone(value: T) { return JSON.parse(JSON.stringify(value)) as T; }, }); } export type AccessTokenClaims = { userId: string; tokenVersion: number; }; function getSecret(config: AppConfig) { return new TextEncoder().encode(config.jwtSecret); } export async function signAccessToken( claims: AccessTokenClaims, config: AppConfig, ) { return new SignJWT({ ver: claims.tokenVersion }) .setProtectedHeader({ alg: 'HS256', typ: 'JWT' }) .setSubject(claims.userId) .setIssuer(config.jwtIssuer) .setIssuedAt() .setExpirationTime(config.jwtExpiresIn) .sign(getSecret(config)); } export async function verifyAccessToken(token: string, config: AppConfig) { try { const { payload } = await jwtVerify(token, getSecret(config), { issuer: config.jwtIssuer, }); const userId = typeof payload.sub === 'string' ? payload.sub : ''; const tokenVersion = typeof payload.ver === 'number' ? payload.ver : NaN; if (!userId || !Number.isFinite(tokenVersion)) { throw unauthorized('JWT 内容无效'); } return { userId, tokenVersion, } satisfies AccessTokenClaims; } catch (error) { throw unauthorized('JWT 校验失败'); } }