Skip to content

Update environment configuration and versioning #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ The `magicpages/ghost-cache-invalidation-proxy` Docker image is available on [Do

### Environment Variables

#### URL Configuration

The proxy supports two URL configurations to handle different deployment scenarios:

- **`GHOST_URL`**: The internal URL used to communicate with your Ghost instance (e.g., `http://ghost:2368` in Docker networks)
- **`GHOST_PUBLIC_URL`** (optional): The public-facing URL of your Ghost site (e.g., `https://yourdomain.com`)

When `GHOST_PUBLIC_URL` is not provided, the proxy will send relative URLs (like `/post-slug`) to your webhook endpoint. When `GHOST_PUBLIC_URL` is configured, the proxy will convert these to absolute URLs (like `https://yourdomain.com/post-slug`).

This is particularly useful for CDNs that require absolute URLs for cache purging, or when your internal Ghost URL differs from your public domain.

#### Required variables

- `GHOST_URL`: The URL of your Ghost CMS instance. Ideally, the hostname of your Ghost container and the port it listens on (e.g., `http://ghost:2368`).
Expand Down Expand Up @@ -57,6 +68,7 @@ services:
image: magicpages/ghost-cache-invalidation-proxy:latest
environment:
- GHOST_URL=http://ghost:2368
- GHOST_PUBLIC_URL=https://yourdomain.com
- PORT=4000
- DEBUG=true
- WEBHOOK_URL=https://api.example.com/invalidate
Expand Down
21 changes: 11 additions & 10 deletions docker-compose.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,17 @@ services:
build:
context: .
environment:
- GHOST_URL=http://ghost:2368
- PORT=4000
- DEBUG=true
- WEBHOOK_URL=https://api.example.com/invalidate
- WEBHOOK_METHOD=POST
- WEBHOOK_SECRET=your_secret_key
- WEBHOOK_HEADERS={"Custom-Header": "Value", "Authorization": "Bearer ${secret}"}
- WEBHOOK_BODY_TEMPLATE={"urls": ${urls}, "timestamp": "${timestamp}", "purgeAll": ${purgeAll}}
- WEBHOOK_RETRY_COUNT=3
- WEBHOOK_RETRY_DELAY=1000
GHOST_URL: http://ghost:2368
GHOST_PUBLIC_URL: https://your-blog.com # Optional: If set, webhook URLs will be absolute
WEBHOOK_URL: https://api.example.com/invalidate
PORT: 4000
DEBUG: "true"
WEBHOOK_METHOD: POST
WEBHOOK_SECRET: your_secret_key
WEBHOOK_HEADERS: '{"AccessKey": "$${secret}", "Content-Type": "application/json"}'
WEBHOOK_BODY_TEMPLATE: '{"urls": $${urls}}'
WEBHOOK_RETRY_COUNT: 3
WEBHOOK_RETRY_DELAY: 1000
ports:
- "4000:4000"
depends_on:
Expand Down
4 changes: 4 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
# The URL of your Ghost CMS instance (required)
GHOST_URL=http://ghost:2368

# Optional: The public-facing URL of your Ghost site (for absolute URL generation in cache purge requests)
# Use this when your internal Ghost URL differs from the public domain
# GHOST_PUBLIC_URL=https://yourdomain.com

# The URL for the webhook endpoint that will be called when cache invalidation is needed (required)
WEBHOOK_URL=https://api.example.com/invalidate

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ghost-cache-invalidation-proxy",
"version": "1.0.1",
"version": "1.1.0",
"description": "A proxy between a Ghost CMS instance and configurable webhooks for cache invalidation.",
"main": "dist/index.js",
"author": "Jannis Fedoruk-Betschki <[email protected]>",
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { MiddlewareConfig } from './types.js'
// Schema for runtime environment validation
const baseSchema = z.object({
GHOST_URL: z.string().url().default('http://localhost:2368'),
GHOST_PUBLIC_URL: z.string().url().optional(),
PORT: z.string().transform(Number).default('3000'),
DEBUG: z.string().transform(val => val === 'true').default('false'),
WEBHOOK_URL: z.string().url(),
Expand Down Expand Up @@ -57,6 +58,7 @@ export function loadConfig(): MiddlewareConfig {

return {
ghostUrl: data.GHOST_URL.replace(/\/$/, ''),
ghostPublicUrl: data.GHOST_PUBLIC_URL?.replace(/\/$/, ''),
port: data.PORT,
debug: data.DEBUG,
webhook: {
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ async function bootstrap() {
const config = loadConfig();
const app = express();

// Disable X-Powered-By header for security
app.disable('x-powered-by');

// Basic security
app.set('trust proxy', config.security.trustProxy);

Expand Down Expand Up @@ -34,6 +37,9 @@ async function bootstrap() {
console.log(`🚀 Server running on port ${config.port}`);
config.debug && console.log('🐛 Debug mode enabled');
console.log(`🔗 Connected to Ghost CMS at ${config.ghostUrl}`);
if (config.ghostPublicUrl) {
console.log(`🌐 Public site URL: ${config.ghostPublicUrl}`);
}
console.log(`📣 Webhook configured at ${config.webhook.url}`);
});
} catch (error: unknown) {
Expand Down
11 changes: 10 additions & 1 deletion src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,17 @@ export class ProxyMiddleware {
console.log('📋 Response headers:', proxyRes.headers);
}

// Filter out potentially sensitive headers
const filteredHeaders = { ...proxyRes.headers };

// Remove headers that might expose server information
delete filteredHeaders['x-powered-by'];
delete filteredHeaders['server'];
delete filteredHeaders['x-aspnet-version'];
delete filteredHeaders['x-aspnetmvc-version'];

// Forward the response headers and status code
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
res.writeHead(proxyRes.statusCode || 200, filteredHeaders);

// Pipe the response body
proxyRes.pipe(res);
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface MiddlewareConfig {
ghostUrl: string;
ghostPublicUrl?: string;
port: number;
debug: boolean;
webhook: {
Expand Down
13 changes: 12 additions & 1 deletion src/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,21 @@ export class WebhookManager {
// - "/post-permalink" - Single post
// - "/, /page/*, /rss" - Multiple pages
// - "/page/*" - Wildcard paths
const urls = purgeAll
let urls = purgeAll
? ['/*']
: pattern.split(',').map(url => url.trim());

// If ghostPublicUrl is configured, convert relative URLs to absolute URLs
if (this.config.ghostPublicUrl && !purgeAll) {
urls = urls.map(url => {
// Only process relative URLs (starting with /)
if (url.startsWith('/')) {
return `${this.config.ghostPublicUrl}${url}`;
}
return url;
});
}

// From Ghost documentation, common patterns include:
// - "/" - Home page
// - "/page/*" - Paginated pages
Expand Down