oa-ai/src/lib/setup-token.ts

49 lines
1.6 KiB
TypeScript

import crypto from 'crypto'
const JWT_SECRET = process.env.JWT_SECRET || 'change-me-same-across-all-sites'
function base64url(str: string): string {
return Buffer.from(str).toString('base64url')
}
export interface SetupTokenPayload {
username: string
purpose: 'password-setup'
iat: number
exp: number
}
export function signSetupToken(username: string, expiresIn: number = 24 * 60 * 60): string {
const header = { alg: 'HS256', typ: 'JWT' }
const now = Math.floor(Date.now() / 1000)
const body: Omit<SetupTokenPayload, 'exp'> & { exp: number } = {
username,
purpose: 'password-setup',
iat: now,
exp: now + expiresIn,
}
const segments = [base64url(JSON.stringify(header)), base64url(JSON.stringify(body))]
const signingInput = segments.join('.')
segments.push(
crypto.createHmac('sha256', JWT_SECRET).update(signingInput).digest('base64url')
)
return segments.join('.')
}
export function verifySetupToken(token: string): { username: string } | null {
try {
const parts = token.split('.')
if (parts.length !== 3) return null
const signingInput = parts.slice(0, 2).join('.')
const expectedSig = crypto.createHmac('sha256', JWT_SECRET)
.update(signingInput).digest('base64url')
if (parts[2] !== expectedSig) return null
const payload: SetupTokenPayload = JSON.parse(
Buffer.from(parts[1], 'base64url').toString()
)
if (payload.purpose !== 'password-setup') return null
if (payload.exp < Math.floor(Date.now() / 1000)) return null
return { username: payload.username }
} catch { return null }
}