UNPKG

@vfarcic/dot-ai

Version:

AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance

107 lines (106 loc) 3.43 kB
"use strict"; /** * JWT signing and verification using node:crypto HMAC-SHA256. * * No external libraries — implements the minimal JWT subset needed * for dot-ai access tokens (HS256 only). */ Object.defineProperty(exports, "__esModule", { value: true }); exports.getJwtSecret = getJwtSecret; exports._resetCachedSecret = _resetCachedSecret; exports.signJwt = signJwt; exports.verifyJwt = verifyJwt; const node_crypto_1 = require("node:crypto"); const JWT_HEADER = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url'); /** Cached auto-generated secret (per-process). */ let cachedSecret; /** * Returns the JWT signing secret. * Uses DOT_AI_JWT_SECRET env var if set, otherwise generates * a random 32-byte hex secret cached for the process lifetime. */ function getJwtSecret() { const envSecret = process.env.DOT_AI_JWT_SECRET; if (envSecret) { return envSecret; } if (!cachedSecret) { cachedSecret = (0, node_crypto_1.randomBytes)(32).toString('hex'); } return cachedSecret; } /** * Reset the cached secret. Only for testing. * @internal */ function _resetCachedSecret() { cachedSecret = undefined; } /** * Sign a JWT with HMAC-SHA256. * * @param claims - The JWT payload claims * @param secret - The signing secret * @returns Encoded JWT string (header.payload.signature) */ function signJwt(claims, secret) { const payload = Buffer.from(JSON.stringify(claims)).toString('base64url'); const data = `${JWT_HEADER}.${payload}`; const signature = (0, node_crypto_1.createHmac)('sha256', secret) .update(data) .digest('base64url'); return `${data}.${signature}`; } /** * Verify a JWT signed with HMAC-SHA256. * * Performs timing-safe signature comparison and expiry check. * * @param token - The JWT string to verify * @param secret - The signing secret * @returns Decoded claims if valid, null otherwise */ function verifyJwt(token, secret) { const parts = token.split('.'); if (parts.length !== 3) { return null; } const [header, payload, signature] = parts; // Recompute expected signature const data = `${header}.${payload}`; const expectedSignature = (0, node_crypto_1.createHmac)('sha256', secret) .update(data) .digest('base64url'); // Timing-safe comparison const sigBuffer = Buffer.from(signature, 'base64url'); const expectedBuffer = Buffer.from(expectedSignature, 'base64url'); if (sigBuffer.length !== expectedBuffer.length) { // Dummy comparison to maintain constant time (0, node_crypto_1.timingSafeEqual)(expectedBuffer, expectedBuffer); return null; } if (!(0, node_crypto_1.timingSafeEqual)(sigBuffer, expectedBuffer)) { return null; } // Decode and validate claims let claims; try { claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')); } catch { return null; } // Validate identity claims if (typeof claims.sub !== 'string' || claims.sub.length === 0) { return null; } if (claims.groups !== undefined && !Array.isArray(claims.groups)) { return null; } // Check expiration (strict: exp === now is treated as expired) const now = Math.floor(Date.now() / 1000); if (typeof claims.exp !== 'number' || claims.exp <= now) { return null; } return claims; }