feat(apiclient): Add native Admin & Content API clients, JWT generator, and tag visibility features; remove external @tryghost deps and update docs
This commit is contained in:
116
ts/apiclient/ghost.jwt.ts
Normal file
116
ts/apiclient/ghost.jwt.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 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}`;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user