@scrape-admin/webhook
v0.2.0
Published
Shared helpers for sending and receiving Scrape Admin webhooks:
Readme
@scrape-admin/webhook
Shared helpers for sending and receiving Scrape Admin webhooks:
- Sending — build the request headers (including token/auth handling) and deliver a
WebhookorWebhookDestination(see@scrape-admin/schema). - Receiving — verify the bearer token and validate the
ResultBagbody Scrape Admin POSTs to destination webhooks, plus a ready-made h3 handler at@scrape-admin/webhook/h3.
Installation
npm install @scrape-admin/webhookSending webhooks
import { defineWebhook, deliverWebhook } from '@scrape-admin/webhook'
const webhook = defineWebhook({
url: 'https://example.com/hooks/scrape-admin',
method: 'POST',
token: 'secret',
tokenType: 'bearer',
})
const response = await deliverWebhook(webhook)Receiving destination webhooks
When a collection entry runs, Scrape Admin POSTs the run's ResultBag as the bare JSON body (no envelope) to each of
its destinations, with Authorization: Bearer <token> when the destination has a token.
With h3 (Nuxt / Nitro)
h3 is an optional peer dependency (^1.15.0); install it if you use this subpath.
// server/api/scrape-admin/[targetId].post.ts
import { defineScrapeAdminWebhookHandler } from '@scrape-admin/webhook/h3'
export default defineScrapeAdminWebhookHandler({
// Look up the target and the secrets it accepts (e.g. current + previous during rotation).
// Return null for an unknown target.
resolveSecrets: async (event) => {
const target = await findTarget(getRouterParam(event, 'targetId'))
return target ? { secrets: [target.secret, target.previousSecret], context: target } : null
},
// Called with a verified, validated result. Its return value is the response.
onResult: async (event, result, target, { bodyHash }) => {
await saveResult(target, result, bodyHash)
return { ok: true }
},
maxBodyBytes: 262_144, // default (256 KiB)
})| Status | When |
| --- | --- |
| 404 | resolveSecrets returned null. |
| 401 | The Authorization header is missing or matches none of the secrets. |
| 413 | The body (or its declared Content-Length) exceeds maxBodyBytes. |
| 422 | The body is not JSON, or not a valid ResultBag (zod issues in data.issues). |
onResult also receives { rawBody, bodyHash }, where bodyHash is the hex SHA-256 of the raw body, which is useful for
idempotency and auditing.
Framework-agnostic
The building blocks are exported from the package root and only use WebCrypto (no node:crypto), so they run on
Node, Cloudflare Workers, Deno and Bun:
import { resultBagSchema, verifyBearerToken } from '@scrape-admin/webhook'
export default {
async fetch(request: Request, env: { CURRENT_SECRET: string, PREVIOUS_SECRET?: string }) {
if (!(await verifyBearerToken(request.headers.get('authorization'), [env.CURRENT_SECRET, env.PREVIOUS_SECRET])))
return new Response('Unauthorized', { status: 401 })
const parsed = resultBagSchema.safeParse(await request.json())
if (!parsed.success) return Response.json({ issues: parsed.error.issues }, { status: 422 })
const { url, attributes, statusCode, error } = parsed.data
// ...
return new Response(null, { status: 204 })
},
}| Export | Description |
| --- | --- |
| verifyBearerToken(header, secrets) | Constant-time check of an Authorization: Bearer header against one or more secrets. Empty secrets are ignored. |
| extractBearerToken(header) | Returns the token from a Bearer <token> header value, or undefined. |
| timingSafeEqual(a, b) | Constant-time Uint8Array comparison. |
| sha256Hex(input) | Hex SHA-256 of a UTF-8 string. |
| resultBagSchema / ResultBag | Lenient zod schema and type for the result body. |
resultBagSchema mirrors ResultBag from @scrape-admin/profile-runner: url is required, variables and
attributes default to {}, redirect, statusCode and error are optional (statusCode/error also accept
null), and unknown keys are kept so newer Scrape Admin versions don't break parsing.
See ./src/index.ts and ./src/h3.ts for the full exported surface.
