Skip to content

@hibiki-js/core ​

Core package for Hibiki. It owns the immutable app builder, the provider contract, request routing, and stable error codes. Provider packages (@hibiki-js/stripe, @hibiki-js/github, …) plug into this API.

Hibiki works with Web Standards Request / Response only. It does not read environment variables, log payloads, queue deliveries, or retry webhooks.

Install ​

bash
pnpm add @hibiki-js/core

Node.js 22+ or any runtime with Web Crypto and Request / Response (Cloudflare Workers, Deno, Bun, …).

Quick example ​

ts
import { Hibiki } from "@hibiki-js/core"
import { stripe } from "@hibiki-js/stripe"

const app = new Hibiki()
  .use(stripe({ secret: process.env.STRIPE_WEBHOOK_SECRET! }))

app.on("stripe.invoice.paid", async ({ event }) => {
  console.log(event.data.object.id)
})

export async function POST(request: Request) {
  return app.handle(request, { provider: "stripe" })
}

Hibiki ​

ts
import { Hibiki } from "@hibiki-js/core"

const app = new Hibiki(options?)

Hibiki is an immutable type-state builder. use() returns a new instance; the previous one is unchanged. After providers are registered, on() only accepts event names that exist on those providers.

Options ​

OptionTypeDefaultDescription
strictEventsbooleanfalseWhen true, unsupported provider events return 400 with HIBIKI_UNSUPPORTED_EVENT instead of 200
maxBodyBytesnumber1048576Maximum raw request bytes before verification; positive safe integer
idempotencyStoreIdempotencyStoreBounded memory storeAtomic authenticated-ID claims; provide shared durable storage across instances

See Secure webhook handling for store contracts, replay limits, and retry behavior.

use(provider) ​

Registers a provider and returns a new Hibiki with an updated type registry.

ts
const app = new Hibiki()
  .use(stripe({ secret }))
  .use(github({ secret: githubSecret }))
  • Throws HIBIKI_PROVIDER_NOT_REGISTERED (status 500) if the same provider.name is registered twice
  • Does not mutate the previous builder

on(eventName, handler) ​

Registers exactly one handler for a typed event such as stripe.invoice.paid or github.pull_request.opened.

ts
app.on("stripe.checkout.session.completed", async (context) => {
  // context.event is narrowed to that Stripe payload
  console.log(context.event.data.object.id)
})

Handler signature:

ts
(context: HibikiContext) => Promise<Response | void> | Response | void
  • Returns this so you can chain registrations
  • Throws HIBIKI_DUPLICATE_HANDLER (status 500) if the event already has a handler
  • If the handler returns a Response, that response is sent as-is
  • If it returns void / undefined, Hibiki responds with 204

useMiddleware(middleware) ​

Adds middleware that wraps registered handler execution only. It does not run for signature failures, unsupported content types, parse errors, or unsupported events.

ts
app.useMiddleware(async (context, next) => {
  const start = performance.now()
  await next()
  console.log(context.provider, context.id, performance.now() - start)
})

Signature:

ts
(context: HibikiContext, next: () => Promise<void>) => Promise<void> | void

Middleware runs in registration order. Always await next() unless you intentionally short-circuit (Hibiki still expects the handler path to complete for a normal response).

handle(request, options) ​

Verifies, parses, and routes one webhook Request, then returns a Response.

ts
await app.handle(request, {
  provider: "stripe",
  waitUntil?: (promise: Promise<unknown>) => void,
})
OptionRequiredDescription
provideryesRegistered provider name ("stripe", "github", …)
waitUntilnoOptional background scheduler (for example Cloudflare Workers ctx.waitUntil). Forwarded onto HibikiContext when set

Pipeline for every call:

  1. Resolve the named provider
  2. Read raw body bytes from a cloned Request
  3. provider.verify(...)
  4. Enforce contentTypes when the provider defines them
  5. provider.parse(...)
  6. Route to the matching handler (with middleware), or return early for unsupported / unhandled events

Default HTTP responses ​

SituationStatusBody
Supported event, handler returns void204empty
Supported event, no handler registered204empty
Unsupported provider event (strictEvents: false)200empty
Unsupported provider event (strictEvents: true)400HIBIKI_UNSUPPORTED_EVENT
Handler returns a Responsethat statusthat body
HibikiError thrownerror.statuserror.code (plain text)
Unexpected non-HibikiError throw500Internal webhook error

200 for unsupported events is intentional: it stops providers such as Stripe / GitHub from retrying forever for event types you have not curated yet.

HibikiContext ​

Passed to every handler and middleware.

FieldTypeDescription
eventtyped payloadParsed provider event for this handler
providerstringProvider name ("stripe", …)
requestRequestOriginal request object
headersHeadersSame as request.headers
rawBodystringDecoded UTF-8 body text used for parsing
idstring?Optional event id when the provider supplies one (Stripe sets this from event.id)
waitUntil((promise: Promise<unknown>) => void)?Present only when handle received waitUntil

defineProvider / HibikiProvider ​

Use defineProvider when shipping a custom provider. It is a typed identity helper — no runtime magic.

ts
import { defineProvider, HibikiError, type ParseResult } from "@hibiki-js/core"

interface Events {
  ping: { ok: true }
}

export const example = defineProvider<"example", Events>({
  name: "example",
  contentTypes: ["application/json"],
  async verify({ headers }) {
    if (!headers.get("x-example-signature")) {
      throw new HibikiError("HIBIKI_VERIFICATION_FAILED", "Missing signature", 400)
    }
  },
  async parse(): Promise<ParseResult<Events>> {
    return { kind: "supported", eventName: "ping", event: { ok: true } }
  },
})

HibikiProvider members ​

MemberDescription
nameUnique provider id. Becomes the prefix in event names (name.event)
contentTypes?Allowed media types checked after verification. Compared case-insensitively against the type before ;
verify(input)Signature / authenticity check. Throw HibikiError on failure
parse(input)Return { kind: "supported", eventName, event, id? } or { kind: "unsupported", nativeEventName, id? }

VerifyInput / ParseInput ​

FieldOnDescription
rawBodybothRaw request bytes (Uint8Array)
headersbothRequest headers
requestbothOriginal Request
textparse onlyUTF-8 decoded body string

HibikiError ​

ts
class HibikiError extends Error {
  readonly code: HibikiErrorCode
  readonly status: number
}

HTTP responses use error.code as the entire response body. Do not put secrets or payload details in HibikiError messages that you expect callers to see — only the code is returned.

CodeTypical statusWhen
HIBIKI_VERIFICATION_FAILED400Signature / authenticity check failed
HIBIKI_PARSE_FAILED400Provider parse threw a non-HibikiError
HIBIKI_UNSUPPORTED_EVENT400Unsupported event with strictEvents: true
HIBIKI_UNSUPPORTED_CONTENT_TYPE415Content-Type not in provider.contentTypes
HIBIKI_PROVIDER_NOT_REGISTERED500Unknown provider name, or duplicate use()
HIBIKI_DUPLICATE_HANDLER500Second on() for the same event
HIBIKI_HANDLER_FAILED500Handler or middleware threw

Useful types ​

TypeRole
EventMapConstraint for provider event maps
ParseResult<TEvents> / SupportedParseResult<TEvents>Provider parse return types
ProviderRegistryMap of registered providers
RegisteredEventName<R>Union of "provider.event" strings for registry R
HibikiOptions / HandleOptions<R>Constructor / handle option types

Security error codes ​

CodeStatus
HIBIKI_PAYLOAD_TOO_LARGE413
HIBIKI_EVENT_IN_PROGRESS503
HIBIKI_IDEMPOTENCY_UNAVAILABLE503