/** * JWT token generator for Ghost Admin API * Implements HS256 signing compatible with Ghost's authentication */ /** * Base64 URL encode (without padding) */ function base64UrlEncode(data: Uint8Array): string { const base64 = typeof Buffer !== 'undefined' ? Buffer.from(data).toString('base64') : btoa(String.fromCharCode(...data)); return base64 .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } /** * Convert hex string to Uint8Array */ function hexToBytes(hex: string): Uint8Array { const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); } return bytes; } /** * Generate JWT token for Ghost Admin API * @param key - Admin API key in format {id}:{secret} * @param audience - Token audience (API prefix like '/admin/') * @returns JWT token string */ export async function generateToken(key: string, audience: string): Promise { // Parse the admin key const [keyId, secret] = key.split(':'); if (!keyId || !secret) { throw new Error('Invalid admin API key format. Expected {id}:{secret}'); } if (keyId.length !== 24 || secret.length !== 64) { throw new Error('Invalid admin API key format. Expected 24 hex chars for id and 64 for secret'); } // Create JWT header const header = { alg: 'HS256', typ: 'JWT', kid: keyId }; // Create JWT payload const now = Math.floor(Date.now() / 1000); const payload = { iat: now, exp: now + 300, // 5 minutes aud: audience }; // Encode header and payload const headerEncoded = base64UrlEncode( new TextEncoder().encode(JSON.stringify(header)) ); const payloadEncoded = base64UrlEncode( new TextEncoder().encode(JSON.stringify(payload)) ); // Create signature data const signatureData = `${headerEncoded}.${payloadEncoded}`; // Convert secret from hex to bytes const secretBytes = hexToBytes(secret); // Import key for HMAC let cryptoKey: CryptoKey; // Try to use Web Crypto API (works in Node 15+ and all modern browsers) try { const crypto = globalThis.crypto || (await import('crypto')).webcrypto; // Convert to proper BufferSource type const secretBuffer = secretBytes.buffer.slice(secretBytes.byteOffset, secretBytes.byteOffset + secretBytes.byteLength) as ArrayBuffer; cryptoKey = await crypto.subtle.importKey( 'raw', secretBuffer, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); // Sign the data const signature = await crypto.subtle.sign( 'HMAC', cryptoKey, new TextEncoder().encode(signatureData) ); // Encode signature const signatureEncoded = base64UrlEncode(new Uint8Array(signature)); // Return complete JWT return `${signatureData}.${signatureEncoded}`; } catch (error) { // Fallback for older Node versions using crypto module directly const crypto = await import('crypto'); const hmac = crypto.createHmac('sha256', secretBytes); hmac.update(signatureData); const signature = hmac.digest(); const signatureEncoded = base64UrlEncode(signature); return `${signatureData}.${signatureEncoded}`; } }