Ghost connector
A standalone Node connector that maps a Ghost publication's posts and pages to bAInquet IngestItems through the Ghost Content API (backfill) and Ghost webhooks (incremental), signs every request with the connector HMAC scheme, and POSTs idempotent items and batches.
The connector lives under connectors/ghost/bainquet-connector and sends X-Connector-Type: ghost. It ships outside the bAInquet monorepo, so it vendors the signer: src/bainquet-sign.ts is a self-contained copy of the canonical signer with only a node:crypto dependency. It requires Node 20.11 or later.
What it maps
| Ghost object | IngestItem type | Stable id | Notes |
|---|---|---|---|
| Post | post | post:<id> | Rendered html plus plaintext, tags, authors, SEO meta, feature image. |
| Page | page | page:<id> | Same field set as posts. |
- URL. Ghost's own absolute URL (its host equals
X-Site-Domain), falling back to<siteOrigin>/<slug>/. - SEO.
meta_title,meta_description, andog_*map intojson.seo; a differing canonical becomes the itemcanonical_url. - Exclusions. Draft and scheduled posts, and members-only or paid content (
visibility != "public"), are excluded to avoid leaking gated content. Internal tags (#-prefixed orvisibility:"internal") are filtered out.
Install
npm install
npm run buildConfigure
All configuration is via environment variables.
| Variable | Purpose |
|---|---|
BAINQUET_API_URL | bAInquet ingest base (default https://api.bainquet.online/v1) |
BAINQUET_CONNECTOR_TOKEN | The connectorId.secret |
BAINQUET_WEBSITE_ID | The scoped website id |
BAINQUET_SITE_DOMAIN | Site host, e.g. acme.com |
GHOST_API_URL | Ghost site URL, e.g. https://acme.com |
GHOST_CONTENT_API_KEY | Ghost Content API key (read-only) for backfill |
GHOST_SITE_ORIGIN | Public origin (default = GHOST_API_URL) |
GHOST_DEFAULT_LANGUAGE | Default en |
GHOST_WEBHOOK_SECRET | Optional URL-path secret (see below) |
How incremental sync works
Ghost webhooks are not cryptographically signed
Unlike Shopify, Wix, or Webflow, Ghost sends no HMAC header on its webhooks, so there is no inbound signature to verify. The realistic guard is a hard-to-guess secret embedded in the webhook URL path (GHOST_WEBHOOK_SECRET) over an HTTPS-only deployment, treating the URL itself as the shared secret.
Ghost also does not send the event name in a header; the event is implied by which webhook URL you configured. Configure each Ghost webhook to its matching path:
POST /webhooks/ghost/<secret>/post.published
POST /webhooks/ghost/<secret>/post.published.edited
POST /webhooks/ghost/<secret>/post.unpublished
POST /webhooks/ghost/<secret>/post.deleted
POST /webhooks/ghost/<secret>/page.published (and .edited / .unpublished / .deleted)node dist/scripts/webhook-server.js # :8475- published and published.edited map the object and POST to
POST /v1/ingest/item. - unpublished and deleted POST to
POST /v1/ingest/delete(tombstone by stable id).
The outbound bAInquet request is still HMAC-signed: that is the connector token's job, separate from the unsigned inbound Ghost webhook.
How backfill works
npm run backfillIt pages the Ghost Content API (posts and pages, with tags and authors, formats=html,plaintext), maps published public objects, and batch-POSTs in chunks of 500. It is idempotent: unchanged items return skipped.
Out of scope
This connector is a content mapper, signer, webhook glue, and backfill. A hosted connection UI, a durable outbox, and llms.txt or ai.json emission would wrap this core and are not included. No LLM is used anywhere.
HMAC signing
Every outbound request is signed exactly as the server verifies it, using the shared bq.connector.hmac.v1 scheme:
signingKey = HKDF-SHA256(secret, salt = websiteId, info = "bq.connector.hmac.v1", 32 bytes)
canonical = METHOD \n path \n sha256(body) \n timestamp \n nonce \n websiteId
X-Signature = hex HMAC-SHA256(signingKey, canonical)Headers include X-Connector-Type: ghost, alongside Authorization: Bearer <token>, X-Site-Domain, X-Connector-Version, X-Signature, X-Timestamp, X-Nonce, X-Body-Sha256, and Idempotency-Key. Retries on 5xx, 429, and network errors reuse the same Idempotency-Key with a fresh nonce and timestamp.
The signing is parity-tested: npm test runs the vendored-signer parity test against the known-good vector, plus mapper and webhook unit tests. The full scheme is documented in Ingestion and signing.