Files
ghost/ts/apiclient/ghost.jwt.ts

117 lines
3.2 KiB
TypeScript

/**
* 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<string> {
// 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}`;
}
}