@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
JavaScript
;
/**
* 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;
}