handleCloudflareWebhook reads request.text() and request.headers, forwards URL.searchParams as query, and returns webhookHttpResultToResponse. No payment logic, no store adapters, no static cloudflare:workers import (structural Request / Response only).
Version 0.1.1 — published on npm. Portable (paymentsSdk.portable: true). Optional peer @cloudflare/workers-types. Export map: "." only. Depends only on @paykernel/integration-http among workspace packages.
bun add @paykernel/integration-cloudflare-workers
# dependency: @paykernel/integration-http
# optional peer: @cloudflare/workers-typesUsage
import {
handleCloudflareWebhook,
readWorkerBindings,
} from "@paykernel/integration-cloudflare-workers";
export default {
async fetch(request: Request, env: Record<string, string | undefined>) {
const { STRIPE_WEBHOOK_SECRET } = readWorkerBindings(env, ["STRIPE_WEBHOOK_SECRET"]);
const url = new URL(request.url);
if (url.pathname === "/webhooks/stripe" && request.method === "POST") {
return handleCloudflareWebhook(request, {
gateway: "stripe",
client, // PaymentClient with no onWebhookVerified fulfillment
engine,
handler,
});
}
return new Response("not_found", { status: 404 });
},
};handleCloudflareWebhook(request, options):
- Non-
POST→new Response("method not allowed", { status: 405 })(plain text, not JSON) rawBody = await request.text()headers = request.headers- Query from
URL.searchParams(first value per key) processWebhookHttp({ ...options, rawBody, headers, query })return webhookHttpResultToResponse(result)
options is Omit<ProcessWebhookHttpInput, "rawBody" | "headers" | "query">.
Correlation: resolveCorrelationId uses x-request-id → x-correlation-id → cf-ray → generated id. When Cloudflare sends cf-ray and no x-request-id, the response x-request-id is that ray id.
Runnable host: Cloudflare Workers fetch. Tests in that example run in Bun with store-sqlite (:memory: is one process, single-host). Do not use that SQLite in a production Worker.
readWorkerBindings
Alias of requireStringBindings from @paykernel/integration-http:
readWorkerBindings(env, ["STRIPE_WEBHOOK_SECRET"]);
// throws `missing env: STRIPE_WEBHOOK_SECRET` — keys only, never valuesEmpty strings count as missing.
createCloudflareWebhookFetchHandler
import { createCloudflareWebhookFetchHandler } from "@paykernel/integration-cloudflare-workers";
const webhookFetch = createCloudflareWebhookFetchHandler({
gateway: "stripe",
client,
engine,
handler,
});
// Still wrap with a pathname check:
export default {
async fetch(request: Request) {
const url = new URL(request.url);
if (url.pathname === "/webhooks/stripe") {
return webhookFetch(request);
}
return new Response("not_found", { status: 404 });
},
};The helper returns 405 for non-POST, then delegates to handleCloudflareWebhook. It is not a complete Worker router.
Failure paths
| Request | Status | Body |
|---|---|---|
GET / non-POST |
405 | "method not allowed" (text) |
Missing stripe-signature |
400 | { error: "invalid_webhook" } — client not called |
| Forgery (bad HMAC) | 400 | { error: "invalid_webhook" } |
Parse / missing-config InvalidWebhookError |
500 | { outcome: "handler_failed", retryable: true } |
already_processing |
503 | { outcome: "already_processing" }; Retry-After when retryAfterMs is set |
payload_conflict |
409 | { outcome: "payload_conflict" } |
| Handler throws | 500 | { outcome: "handler_failed", retryable: true } |
Missing env key in readWorkerBindings |
throws | missing env: KEY |
Default ackPolicy is provider_redelivery (scheduled_for_retry → 503). { kind: "durable_worker" } ACKs 200 only with engine.mode === "durable_retry" and workerGuaranteed === true.
Status codes come from mapInboxOutcome in @paykernel/integration-http, not from @paykernel/webhooks.
Stores on Workers
D1 ≠ Durable Objects ≠ Turso ≠ local SQLite. Production Workers inject D1 or Durable Objects via the checkout kernel’s stores / storeFactory / executor — never one global Durable Object, never local SQLite as multi-host, never Turso /sync (that export does not exist). Memory stores are NON-PRODUCTION. No published adapter declares coordinationScope: "multi-region".
Re-exports
import {
handleCloudflareWebhook,
readWorkerBindings,
createCloudflareWebhookFetchHandler,
mapInboxOutcome,
retryAfterSeconds,
processWebhookHttp,
webhookHttpResultToResponse,
createWebhookOperationContext,
getHeader,
resolveCorrelationId,
requireStringBindings,
GATEWAY_WEBHOOK_SIGNATURE,
extractWebhookSignature,
} from "@paykernel/integration-cloudflare-workers";Types re-exported: InboxHttpAckPolicy, HeaderBag, GatewayWebhookSignatureProfile, WebhookClient, WebhookHttpResult, ProcessWebhookHttpInput.
OBJECT_HMAC_GATEWAYS is not re-exported here. Import it from @paykernel/integration-http.
Example POST /internal/* routes used by the checkout kernel tests are unauthenticated test hooks (enableTestHooks) and must not be deployed.