All files / web/src/lib guest-token.ts

100% Statements 73/73
90.9% Branches 10/11
100% Functions 3/3
100% Lines 73/73

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 741x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 23x 23x 23x 1x 1x 22x 22x 1x 1x 1x 1x 1x 1x 1x 1x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 1x 1x 1x 1x 1x 1x 1x 1x 12x 12x 12x 12x 12x 12x 12x 9x 12x 2x 2x 7x 7x 7x 7x 7x 7x 12x 5x 5x 12x  
import { jwtVerify, SignJWT } from 'jose'
 
/**
 * Guest token utilities for stateless guest session management
 *
 * Uses HttpOnly cookies with signed JWTs to track guest identities.
 * Tokens are small (just a stable ID) to respect cookie size limits.
 */
 
// Cookie name with __Host- prefix for security in production
// __Host- prefix requires: Secure, Path=/, no Domain
// In development (http://localhost), __Host- won't work without Secure flag
export const GUEST_COOKIE_NAME = process.env.NODE_ENV === 'production' ? '__Host-guest' : 'guest'
 
/**
 * Get the secret key for signing/verifying JWTs
 */
function getKey(): Uint8Array {
  const secret = process.env.AUTH_SECRET
  if (!secret) {
    throw new Error('AUTH_SECRET environment variable is required')
  }
  return new TextEncoder().encode(secret)
}
 
/**
 * Create a signed guest token (JWT)
 *
 * @param sid - Stable session ID (UUID or similar)
 * @param maxAgeSec - Token expiration in seconds (default: 30 days)
 * @returns Signed JWT string
 */
export async function createGuestToken(
  sid: string,
  maxAgeSec = 60 * 60 * 24 * 30 // 30 days
): Promise<string> {
  const now = Math.floor(Date.now() / 1000)
 
  return await new SignJWT({ typ: 'guest', sid })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt(now)
    .setExpirationTime(now + maxAgeSec)
    .sign(getKey())
}
 
/**
 * Verify and decode a guest token
 *
 * @param token - JWT string from cookie
 * @returns Decoded payload with sid, iat, exp
 * @throws Error if token is invalid or expired
 */
export async function verifyGuestToken(token: string): Promise<{
  sid: string
  iat: number
  exp: number
}> {
  try {
    const { payload } = await jwtVerify(token, getKey())
 
    if (payload.typ !== 'guest' || typeof payload.sid !== 'string') {
      throw new Error('Invalid guest token payload')
    }
 
    return {
      sid: payload.sid as string,
      iat: payload.iat as number,
      exp: payload.exp as number,
    }
  } catch (error) {
    throw new Error(`Guest token verification failed: ${error}`)
  }
}