Skip to content

@paykernel/integration-cloudflare-workers

Thin Workers fetch adapter using request.text() and handleCloudflareWebhook. Path guard required.

Updated View as Markdown

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-types

Usage

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):

  1. Non-POSTnew Response("method not allowed", { status: 405 }) (plain text, not JSON)
  2. rawBody = await request.text()
  3. headers = request.headers
  4. Query from URL.searchParams (first value per key)
  5. processWebhookHttp({ ...options, rawBody, headers, query })
  6. return webhookHttpResultToResponse(result)

options is Omit<ProcessWebhookHttpInput, "rawBody" | "headers" | "query">.

Correlation: resolveCorrelationId uses x-request-idx-correlation-idcf-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 values

Empty 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close