Files
Genarrative/server-node/src/auth/token.ts
2026-04-10 15:37:02 +08:00

64 lines
1.6 KiB
TypeScript

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<T>(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 校验失败');
}
}