import { SignJWT, jwtVerify, errors, type JWTPayload } from "jose"; import { compare } from "bcryptjs"; export class TokenExpiredError extends Error { constructor(message = "Token has expired") { super(message); this.name = "TokenExpiredError"; } } const ACCESS_TOKEN_LIFETIME = "120m"; const REFRESH_TOKEN_LIFETIME = "90d"; function createSecretKey(secret: string): Uint8Array { return new TextEncoder().encode(secret); } export async function createAccessToken( jwtSecret: string, userDid: string, serviceDid: string, ): Promise { const secret = createSecretKey(jwtSecret); return new SignJWT({ scope: "com.atproto.access" }) .setProtectedHeader({ alg: "HS256", typ: "at+jwt" }) .setIssuedAt() .setAudience(serviceDid) .setSubject(userDid) .setExpirationTime(ACCESS_TOKEN_LIFETIME) .sign(secret); } export async function createRefreshToken( jwtSecret: string, userDid: string, serviceDid: string, ): Promise { const secret = createSecretKey(jwtSecret); const jti = crypto.randomUUID(); return new SignJWT({ scope: "com.atproto.refresh" }) .setProtectedHeader({ alg: "HS256", typ: "refresh+jwt" }) .setIssuedAt() .setAudience(serviceDid) .setSubject(userDid) .setJti(jti) .setExpirationTime(REFRESH_TOKEN_LIFETIME) .sign(secret); } export async function verifyAccessToken( token: string, jwtSecret: string, serviceDid: string, ): Promise { const secret = createSecretKey(jwtSecret); let payload: JWTPayload; let protectedHeader: { typ?: string }; try { const result = await jwtVerify(token, secret, { audience: serviceDid, }); payload = result.payload; protectedHeader = result.protectedHeader; } catch (err) { if (err instanceof errors.JWTExpired) { throw new TokenExpiredError(); } throw err; } if (protectedHeader.typ !== "at+jwt") { throw new Error("Invalid token type"); } if (payload.scope !== "com.atproto.access") { throw new Error("Invalid scope"); } return payload; } export async function verifyRefreshToken( token: string, jwtSecret: string, serviceDid: string, ): Promise { const secret = createSecretKey(jwtSecret); let payload: JWTPayload; let protectedHeader: { typ?: string }; try { const result = await jwtVerify(token, secret, { audience: serviceDid, }); payload = result.payload; protectedHeader = result.protectedHeader; } catch (err) { if (err instanceof errors.JWTExpired) { throw new TokenExpiredError(); } throw err; } if (protectedHeader.typ !== "refresh+jwt") { throw new Error("Invalid token type"); } if (payload.scope !== "com.atproto.refresh") { throw new Error("Invalid scope"); } if (!payload.jti) { throw new Error("Missing token ID"); } return payload; } export { compare as verifyPassword };