2025-03-15 16:30:26 +01:00

58 lines
2.1 KiB
TypeScript

import type { PathLike } from 'node:fs';
import fs from 'node:fs/promises';
import * as Sentry from '@sentry/node';
import { importPKCS8, importSPKI, jwtVerify, SignJWT } from 'jose';
import type { KeyLike, JWTPayload } from 'jose';
import { rootDir } from './paths.ts';
import { getUrlForLocale, getUrlsForAllLocales } from '~/src/domain.ts';
class Jwt {
constructor(private privateKey: KeyLike, private publicKey: KeyLike) {}
static async from(privateKeyPath: PathLike, publicKeyPath: PathLike) {
const privateKeyPromise = fs.readFile(privateKeyPath, 'utf-8')
.then((privateKeyContent) => importPKCS8(privateKeyContent, 'RS256'));
const publicKeyPromise = fs.readFile(publicKeyPath, 'utf-8')
.then((publicKeyContent) => importSPKI(publicKeyContent, 'RS256'));
const [privateKey, publicKey] = await Promise.all([privateKeyPromise, publicKeyPromise]);
return new Jwt(privateKey, publicKey);
}
async sign(
locale: string,
payload: JWTPayload,
expiresIn = '365d',
domainBase: string | undefined = undefined,
): Promise<string> {
return await new SignJWT(payload)
.setProtectedHeader({ alg: 'RS256' })
.setExpirationTime(expiresIn)
.setAudience(getUrlsForAllLocales(locale, false, domainBase))
.setIssuer(getUrlForLocale(locale, domainBase))
.sign(this.privateKey);
}
async validate<PayloadType = JWTPayload>(
locale: string,
token: string,
domainBase: string | undefined = undefined,
): Promise<PayloadType | undefined> {
const urls = getUrlsForAllLocales(locale, false, domainBase);
try {
const { payload } = await jwtVerify<PayloadType>(token, this.publicKey, {
algorithms: ['RS256'],
audience: urls,
issuer: urls,
});
return payload;
} catch (error) {
Sentry.captureException(error);
}
}
}
export default await Jwt.from(`${rootDir}/keys/private.pem`, `${rootDir}/keys/public.pem`);