AGENTS.md@packages/integration-cloudflare-worker · git:20260813.8f9d732 · 2026-08-13 · sha256 b979f10b89956fb3

AGENTS.md@packages/integration-cloudflare-worker git:20260813.8f9d732A

Immutable. This exact content is served forever at /api/v1/blob/b979f10b89956fb3.

# integration-cloudflare-worker

## Purpose

Cloudflare traffic integration. Generates the JavaScript Worker that the
operator deploys onto their Cloudflare zone; verifies the HMAC-signed
inbound ingest requests it produces; normalizes Worker events into the
provider-neutral `NormalizedTrafficRequest` shape consumed by
`packages/integration-traffic`.

The adapter supports both delivery modes. With **direct push**, the customer's
Worker sends each filtered request to a canonry ingest endpoint. With
**queue pull**, the Worker publishes the same event batch to a Cloudflare Queue
and the single-team canonry deployment drains it over the HTTP pull API. The
Queue Worker needs only a producer binding; the Queue API token remains in the
canonry credential store.

The push direction is safe because canonry is single-tenant per
deployment — the Worker only ever talks to the operator's own canonry
instance, never to a canonry-hosted SaaS relay.

## Key Files

| File | Role |
|------|------|
| `src/script.ts` | `generateWorkerScript` — produces an ES-module Worker with a transport-neutral capture path and direct-push or Queue delivery. `generateWranglerToml` emits non-secret vars and the account id, but never attaches a route. Operators attach the exact route manually with Fail open. |
| `src/canonical-json.ts` | Deterministic JSON encoding embedded into the generated Worker and reused by receiver signature verification. |
| `src/normalize.ts` | `normalizeCloudflareEdgeEvent` — one edge event → `NormalizedTrafficRequest`; the old Worker-named export is a compatibility alias. |
| `src/verify.ts` | `verifyRequestSignature` — timestamp window + HMAC-SHA256 check. Constant-time once inputs are well-formed. |
| `src/types.ts` | `CloudflareWorkerBotList`, `GenerateWorkerScriptOptions` |
| `src/index.ts` | Re-exports public API |

## Patterns

- **Edge filter is generic; canonry classifier is strict.** The Worker
  forwards on a broad UA keyword match, an exact canonical AI-engine domain
  (including subdomains), AI `utm_source` evidence on the request or referrer,
  or Cloudflare bot signals (`cf.botManagement.verifiedBot` /
  `cf.botManagement.score`). The authoritative bot-id / operator
  decisions happen in `packages/integration-traffic` once the event lands
  server-side. Updating the strict list does not require a Worker
  redeploy; updating the generic list does.
- **Versioned bot list.** `CloudflareWorkerBotList.version` is baked into
  the generated script and stored on the source row as
  `configJson.expectedBotListVersion`. The Worker reports its
  `workerVersion` on every ingest call; the receiver records it on
  `traffic_sources.lastWorkerVersion`. The `traffic.source.worker-version`
  doctor check compares it with `configJson.workerVersion`.
- **HMAC-SHA256 with timestamp binding.** The Worker signs
  `timestamp + "." + canonicalJson(batch)` with the per-source HMAC secret and sends
  `X-Canonry-Timestamp` + `X-Canonry-Signature`. The receiver verifies a
  ±300s window then runs constant-time equality. Failure reasons are
  intentionally specific (`timestamp_invalid` / `timestamp_expired` /
  `signature_invalid` / `signature_mismatch`) for receiver-side logging,
  but **never echoed back to the Worker** — an attacker who knows which
  leg failed can enumerate the rest.
- **Direct-push secrets live in `~/.canonry/config.yaml` and Worker secret
  bindings.** The DB stores only the sha256 of the bearer
  (`traffic_sources.ingestTokenHash`). The HMAC secret never goes to the DB in
  any form. Queue pull uses no Worker secret binding; its Cloudflare API token
  stays server-side in the canonry credential store. No delivery mode emits a
  secret in generated source, Wrangler TOML, API output, or MCP output.
- **ES-module Worker + `waitUntil`.** The generated Worker exports
  `{ fetch(request, env, ctx) }` and uses `ctx.waitUntil(...)` so delivery never blocks the customer
  response. Errors are swallowed — AI traffic is statistical, not
  transactional; dropped events are acceptable, and surfacing the failure
  would mask the customer response.
- **Delivery is the seam.** Filtering builds a `CloudflareEdgeEventBatch`,
  then `deliverEdgeEventBatch` selects `deliverViaDirectPush` or
  `deliverViaQueue`. Delivery modes share the filter, event schema, canonical
  encoding, and normalizer.
- **`cf-ray` as event id.** Cloudflare assigns a unique `cf-ray` per
  request. The normalizer namespaces it as `cloudflare-worker:<ray>` so
  it cannot collide with another adapter's event id.
- **`cf-connecting-ip` enables IP verification.** Cloudflare exposes the
  real client IP on every plan via this header, so unlike the Vercel
  adapter, Cloudflare-Worker sources can promote `claimed_unverified` →
  `verified` via `packages/integration-traffic/src/ip-verify.ts`.
- **No classification, no DB, no I/O.** This package only generates,
  normalizes, and verifies. The HTTP route + DB writes live in
  `packages/api-routes/src/traffic.ts`. The classifier + rollup live in
  `packages/integration-traffic`.

## Common Mistakes

- **Echoing the verifier's failure reason in the HTTP response.** Use a
  single 401 envelope; do not let the Worker (or anything else) learn
  which leg of the auth failed.
- **Putting a direct-push secret or Queue API token in generated source/TOML or
  `traffic_sources.configJson`.** Direct-push shared secrets belong in
  `~/.canonry/config.yaml`; only the bearer hash goes to the DB. The Queue API
  token also stays in the local credential store and never becomes a Worker
  binding.
- **Adding bot-id or operator classification in this package.** The
  classifier lives in `packages/integration-traffic` for one-place rule
  evolution across every adapter.
- **Storing or reading the Worker bot list anywhere but `DEFAULT_BOT_LIST`.**
  The Worker is regenerated from this constant; updates must rev the
  `version` field so the staleness check picks up the drift.

## See Also

- `packages/contracts/src/traffic.ts` — `cloudflareEdgeEventSchema`,
  `cloudflareEdgeEventBatchSchema`, `cloudflareTrafficSourceConfigSchema`,
  plus the compatibility Worker-named aliases,
  `trafficConnectCloudflareRequestSchema`,
  `trafficConnectCloudflareResponseSchema`
- `packages/integration-traffic/AGENTS.md` — classifier + rollup that the
  ingest route hands off to
- `packages/integration-vercel/AGENTS.md` — sibling adapter (pull, not
  push) — mirror file layout, different delivery shape