{"_id":"@42flowsdotcom/webhook","_rev":"2-9083111483fc77e2197f7b87edc053b2","name":"@42flowsdotcom/webhook","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@42flowsdotcom/webhook","version":"0.1.0","keywords":["42flows","webhook","content","publish","express","next","nuxt","h3","sveltekit","astro"],"license":"MIT","_id":"@42flowsdotcom/webhook@0.1.0","maintainers":[{"name":"42flows.com","email":"jain@42flows.com"}],"dist":{"shasum":"5935ab96253400e24bbf8aa592dfba06d8ca42fa","tarball":"https://registry.npmjs.org/@42flowsdotcom/webhook/-/webhook-0.1.0.tgz","fileCount":16,"integrity":"sha512-sqkeS4LpqlS7A52l3u6S6lZOou8boI/xD0qj4nQYEJ2d5gme1D9RZgUDVcexkRzb/Kll6mSR9oJPDRM57TwldA==","signatures":[{"sig":"MEYCIQDWXgjq91fp4rt+dCemsCQ9+I0uWbkjgeRU7KwopqeyTAIhAPmuiO3FUwHToQX+ZOpIlhwsFhIkQNU9hPg/OfFBsSx+","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":26354},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.mts","exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs"},"./web":{"types":"./dist/web.d.mts","import":"./dist/web.mjs"},"./next":{"types":"./dist/next.d.mts","import":"./dist/next.mjs"},"./express":{"types":"./dist/express.d.mts","import":"./dist/express.mjs"}},"gitHead":"1acc15d350f0c6956882408e9d6fa847f09b2bc2","scripts":{"build":"unbuild","prepack":"unbuild"},"_npmUser":{"name":"42flows.com","email":"jain@42flows.com"},"_npmVersion":"10.9.4","description":"Receive 42flows-published articles via webhook in any JS framework. Handles Bearer auth, ping, payload validation, response shaping — you only write persistence.","directories":{},"_nodeVersion":"22.22.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"unbuild":"^2.0.0","typescript":"^5.4.0"},"_npmOperationalInternal":{"tmp":"tmp/webhook_0.1.0_1777468618354_0.12622797054906054","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@42flowsdotcom/webhook","version":"0.1.1","description":"Receive 42flows-published articles via webhook in any JS framework. Handles Bearer auth, ping, payload validation, response shaping; you only write persistence.","repository":{"type":"git","url":"git+https://github.com/on-play/42flows.com.git","directory":"packages/webhook"},"type":"module","exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs"},"./express":{"types":"./dist/express.d.mts","import":"./dist/express.mjs"},"./next":{"types":"./dist/next.d.mts","import":"./dist/next.mjs"},"./web":{"types":"./dist/web.d.mts","import":"./dist/web.mjs"}},"main":"./dist/index.mjs","types":"./dist/index.d.mts","scripts":{"build":"unbuild","prepack":"unbuild"},"publishConfig":{"access":"public"},"devDependencies":{"typescript":"^5.4.0","unbuild":"^2.0.0"},"keywords":["42flows","webhook","content","publish","express","next","nuxt","h3","sveltekit","astro"],"license":"MIT","gitHead":"a910ed7dcdd030628386556170c401c9a253e5b2","_id":"@42flowsdotcom/webhook@0.1.1","bugs":{"url":"https://github.com/on-play/42flows.com/issues"},"homepage":"https://github.com/on-play/42flows.com#readme","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-RI6aTTbDWcIPOy/ipwJzUOC7FN0SSonBzi2Mv8pFoS5EZ7+oNHLprBoRaF2+cK46C/Uy8wCaB4WJvWx5YjKvMQ==","shasum":"ff9a269aeceea3e74aa755f4c073b35901a00233","tarball":"https://registry.npmjs.org/@42flowsdotcom/webhook/-/webhook-0.1.1.tgz","fileCount":16,"unpackedSize":27389,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIFUPpk/wAgDhMWwOh9t9KPAquHBNw7m/v/C66lOkvrvwAiAmPXXnpB2E8d4JsK1jE6lF+GLK77PjHeaTKeyZF6sesg=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d43028f6-5bab-4ba9-9e4f-18eae6a5e075"}},"directories":{},"maintainers":[{"name":"42flows.com","email":"jain@42flows.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/webhook_0.1.1_1779026771327_0.0923983996519504"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-29T13:16:58.224Z","modified":"2026-05-17T14:06:11.625Z","0.1.0":"2026-04-29T13:16:58.499Z","0.1.1":"2026-05-17T14:06:11.516Z"},"license":"MIT","keywords":["42flows","webhook","content","publish","express","next","nuxt","h3","sveltekit","astro"],"description":"Receive 42flows-published articles via webhook in any JS framework. Handles Bearer auth, ping, payload validation, response shaping; you only write persistence.","maintainers":[{"name":"42flows.com","email":"jain@42flows.com"}],"readme":"# @42flowsdotcom/webhook\n\nReceive 42flows-published articles via webhook. Library handles Bearer auth, ping, payload parsing, validation, and response shaping. **You write only the persistence callback.**\n\n> Use this when your stack isn't WordPress / Shopify / Nuxt Content. For Nuxt Content sites, install [`@42flowsdotcom/nuxt-content`](https://www.npmjs.com/package/@42flowsdotcom/nuxt-content) instead.\n\n## ⚠️ READ FIRST: which body field do you store?\n\nEvery `Flows42Article` carries BOTH `content_markdown` and `content_html`. They are **always** populated. Pick one. Picking wrong is the #1 source of customer pain.\n\n- Custom DB-backed blog (Express / Next / SvelteKit / custom CMS / WordPress via webhook): **use `article.content_html`**. It's pre-rendered semantic HTML with inline styling and `x42-*` class names. Drops into any HTML renderer, no MDC processor needed.\n- Nuxt Content / Astro with MDX / any renderer that understands `::component` blocks: use `article.content_markdown`. Components hydrate as actual rich UI.\n\n**Do not strip MDC blocks from `content_markdown`.** If you find yourself writing regex to remove `::hook-statistic`, `::strategy-list`, `::powered-by`, `::json-ld`, you're solving a problem that doesn't exist. The same payload has `content_html`, pre-rendered, no MDC. Read that field instead.\n\n## Install\n\n```bash\nnpm install @42flowsdotcom/webhook\n```\n\n## Quick start (Express)\n\n```ts\nimport express from 'express'\nimport { flows42Webhook } from '@42flowsdotcom/webhook/express'\n\nconst app = express()\napp.use(express.json({ limit: '5mb' }))\n\napp.post('/api/42flows/webhook', flows42Webhook({\n  apiKey: process.env.FLOWS42_API_KEY!,\n  onPublish: async (article) => {\n    // Persist however you want (DB, file, search index, queue).\n    await db.blog_posts.upsert({\n      where: { slug: article.slug },\n      create: {\n        slug: article.slug,\n        title: article.title,\n        body_html: article.content_html,        // or content_markdown\n        meta_title: article.meta_title,\n        meta_description: article.meta_description,\n        published_at: new Date(article.published_at),\n        source: '42flows',\n      },\n      update: { /* same fields */ },\n    })\n    return { postUrl: `https://yoursite.com/blog/${article.slug}` }\n  },\n}))\n\napp.listen(3000)\n```\n\nThat's it. The library handles auth, ping, validation, error responses. Your `onPublish` callback runs only when a real article arrives, with a fully-typed `article` object. Returning a `postUrl` lets 42flows track where the article landed (used for delivery verification + dashboards).\n\n## Mental model\n\n42flows publishes articles. You expose one HTTPS endpoint. We POST. Your `onPublish` callback decides what happens next.\n\nThe library prevents the most common bugs:\n- **Auth check**: Bearer token comparison via constant-time compare\n- **Ping short-circuit**: connection-test pings reply 2xx without ever reaching your `onPublish`\n- **Payload validation**: `data.article.slug` + body presence checked before your callback runs\n- **Error response shape**: 4xx / 5xx responses include diagnostic body so 42flows surfaces clear errors in the dashboard\n\n## Adapters\n\nPick the adapter that matches your framework. The customer-side code (`onPublish`) is identical across all of them.\n\n### Express / Connect\n\n```ts\nimport { flows42Webhook } from '@42flowsdotcom/webhook/express'\n\napp.use(express.json({ limit: '5mb' }))\napp.post('/api/42flows/webhook', flows42Webhook({ apiKey, onPublish }))\n```\n\n### Next.js (app router)\n\n```ts\n// app/api/42flows/webhook/route.ts\nimport { flows42NextRoute } from '@42flowsdotcom/webhook/next'\n\nexport const POST = flows42NextRoute({\n  apiKey: process.env.FLOWS42_API_KEY!,\n  onPublish: async (article) => {\n    await db.posts.upsert(/* ... */)\n    return { postUrl: `https://yoursite.com/blog/${article.slug}` }\n  },\n})\n```\n\n### SvelteKit\n\n```ts\n// src/routes/api/42flows/webhook/+server.ts\nimport { flows42WebRouteHandler } from '@42flowsdotcom/webhook/web'\nimport { FLOWS42_API_KEY } from '$env/static/private'\n\nexport const POST = flows42WebRouteHandler({\n  apiKey: FLOWS42_API_KEY,\n  onPublish: async (article) => {\n    await db.posts.upsert(/* ... */)\n    return { postUrl: `/blog/${article.slug}` }\n  },\n})\n```\n\n### Astro\n\n```ts\n// src/pages/api/42flows/webhook.ts\nimport type { APIRoute } from 'astro'\nimport { flows42WebRouteHandler } from '@42flowsdotcom/webhook/web'\n\nconst handler = flows42WebRouteHandler({\n  apiKey: import.meta.env.FLOWS42_API_KEY,\n  onPublish: async (article) => {\n    // Persist however\n    return { postUrl: `https://yoursite.com/blog/${article.slug}` }\n  },\n})\n\nexport const POST: APIRoute = ({ request }) => handler(request)\n```\n\n### Hono\n\n```ts\nimport { Hono } from 'hono'\nimport { flows42WebRouteHandler } from '@42flowsdotcom/webhook/web'\n\nconst handler = flows42WebRouteHandler({\n  apiKey: process.env.FLOWS42_API_KEY!,\n  onPublish: async (article) => {\n    /* ... */\n    return { postUrl: `https://yoursite.com/blog/${article.slug}` }\n  },\n})\n\nconst app = new Hono()\napp.post('/api/42flows/webhook', (c) => handler(c.req.raw))\n```\n\n### Cloudflare Workers / Bun.serve / Deno\n\n```ts\nimport { flows42WebRouteHandler } from '@42flowsdotcom/webhook/web'\n\nconst handler = flows42WebRouteHandler({\n  apiKey: env.FLOWS42_API_KEY,\n  onPublish: async (article) => { /* ... */ },\n})\n\nexport default {\n  async fetch(request: Request): Promise<Response> {\n    if (new URL(request.url).pathname === '/api/42flows/webhook') {\n      return handler(request)\n    }\n    return new Response('Not Found', { status: 404 })\n  },\n}\n```\n\n## The article shape\n\n```ts\ninterface Flows42Article {\n  slug: string                        // URL-safe, max 80 chars\n  title: string\n  content_type: 'guide' | 'strategy' | 'comparison' | 'problem_solution' | 'concept' | 'tool'\n  meta_title: string                  // ≤60 chars\n  meta_description: string            // ≤160 chars\n  primary_keyword: string\n  secondary_keywords: string[]\n  published_at: string                // ISO 8601\n  content_markdown: string            // MDC markdown body. Use if you store markdown\n  content_html: string                // Semantic HTML body. Use if you render HTML\n  json_ld: Array<Record<string, unknown>>  // schema.org Article + FAQ + HowTo blocks\n  backbone: Record<string, unknown>   // Full structured backbone (advanced renderers)\n}\n```\n\nAll fields are always present. Pick `content_markdown` OR `content_html` for your storage; ignore the other.\n\n## Options reference\n\n```ts\nflows42Webhook({\n  // Required\n  apiKey: 'string',\n  onPublish: async (article, meta) => { /* return { postUrl? } */ },\n\n  // Optional\n  onPing: async () => { /* invoked on connection-test pings; library replies 2xx regardless */ },\n  onError: async ({ stage, message, httpStatus }) => {\n    // Invoked on any 4xx/5xx response. Useful for structured logging.\n    // stage: 'auth' | 'parse' | 'validate' | 'persist'\n  },\n})\n```\n\n## How 42flows talks to your endpoint\n\n### Connection test (ping)\n\nWhen you connect a webhook site in the 42flows dashboard, 42flows POSTs:\n\n```\nPOST <your-webhook-url>\nAuthorization: Bearer <FLOWS42_API_KEY>\nContent-Type: application/json\n\n{ \"event_type\": \"ping\", \"timestamp\": \"...\", \"data\": { \"message\": \"...\" } }\n```\n\nThe library short-circuits this and replies 200 `{ ok: true, event_type: \"ping\" }`. Your `onPublish` is never called for pings.\n\n### Real article delivery\n\n```\nPOST <your-webhook-url>\nAuthorization: Bearer <FLOWS42_API_KEY>\nContent-Type: application/json\n\n{\n  \"event_type\": \"publish_article\",\n  \"timestamp\": \"...\",\n  \"flow_type\": \"origin\",\n  \"data\": {\n    \"article\": { /* Flows42Article, see shape above */ }\n  }\n}\n```\n\nThe library validates auth + payload, then calls `await onPublish(article, meta)`. Your callback's return value (`{ postUrl }`) becomes the response 42flows tracks.\n\n## Errors\n\nThe library returns these on its own (your `onPublish` is never invoked):\n\n| Status | When |\n|---|---|\n| 401 | Missing or wrong Bearer token |\n| 400 | Body isn't valid JSON, or `event_type` missing/unknown |\n| 400 | `data.article.slug` missing, or both `content_markdown` and `content_html` empty |\n\nIf your `onPublish` throws, the library returns 500 with the exception message. 42flows captures the response body (truncated to 2KB) and shows it in the activity log so you can debug.\n\n## Migration from a hand-rolled handler\n\nIf you already have a working webhook handler, the library replaces about 30 lines of boilerplate with 5 lines of persistence logic. Before:\n\n```ts\napp.post('/api/42flows/webhook', async (req, res) => {\n  const auth = req.headers.authorization\n  if (auth !== `Bearer ${process.env.FLOWS42_API_KEY}`) {\n    return res.status(401).json({ error: 'unauthorized' })\n  }\n  if (req.body.event_type === 'ping') {\n    return res.status(200).json({ ok: true })\n  }\n  if (req.body.event_type !== 'publish_article') {\n    return res.status(400).json({ error: 'bad event_type' })\n  }\n  const article = req.body.data?.article\n  if (!article?.slug) {\n    return res.status(400).json({ error: 'missing slug' })\n  }\n  // ... actually persist\n})\n```\n\nAfter:\n\n```ts\napp.post('/api/42flows/webhook', flows42Webhook({\n  apiKey: process.env.FLOWS42_API_KEY!,\n  onPublish: async (article) => {\n    // ... actually persist\n    return { postUrl: `https://yoursite.com/blog/${article.slug}` }\n  },\n}))\n```\n\nAuth, ping, parsing, validation, error responses: all gone from your code. Library handles them with the exact contract 42flows backend expects.\n\n## License\n\nMIT\n","readmeFilename":"README.md","homepage":"https://github.com/on-play/42flows.com#readme","repository":{"type":"git","url":"git+https://github.com/on-play/42flows.com.git","directory":"packages/webhook"},"bugs":{"url":"https://github.com/on-play/42flows.com/issues"}}