Skip to content

Latest commit

 

History

History

README.md

distilled-cloudflare

A fully typed Cloudflare SDK for Effect, generated from the Cloudflare OpenAPI specification.

Features

  • Generated from OpenAPI spec — 1:1 compatibility with Cloudflare APIs
  • Typed errors — All errors are TaggedError classes for pattern matching
  • Effect-native — All operations return Effect<A, E, R> with typed errors
  • Automatic pagination — Stream pages or items with .pages() and .items()

Installation

npm install distilled-cloudflare effect @effect/platform

Quick Start

import { Effect, Layer } from "effect";
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
import * as R2 from "distilled-cloudflare/r2";
import { Auth } from "distilled-cloudflare";

const program = R2.listBuckets({ account_id: "your-account-id" });

const CloudflareLive = Layer.mergeAll(FetchHttpClient.layer, Auth.fromEnv());

program.pipe(Effect.provide(CloudflareLive), Effect.runPromise);

Services

import * as R2 from "distilled-cloudflare/r2";
import * as KV from "distilled-cloudflare/kv";
import * as Workers from "distilled-cloudflare/workers";
import * as Queues from "distilled-cloudflare/queues";
import * as Workflows from "distilled-cloudflare/workflows";
import * as DNS from "distilled-cloudflare/dns";

Authentication

import { Auth } from "distilled-cloudflare";

// From environment (CLOUDFLARE_API_TOKEN or CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL)
Effect.provide(Auth.fromEnv())

// Static token
Effect.provide(Auth.fromToken("your-api-token"))

// OAuth
Effect.provide(Auth.fromOAuth({
  clientId: "...",
  clientSecret: "...",
  refreshToken: "...",
}))

Error Handling

import { Effect } from "effect";
import * as R2 from "distilled-cloudflare/r2";

R2.getBucket({ account_id: "...", bucket_name: "missing" }).pipe(
  Effect.catchTags({
    NoSuchBucket: (e) => Effect.succeed({ found: false }),
    CloudflareNetworkError: (e) => Effect.fail(new Error("Network error")),
  }),
);

Pagination

// Stream all pages
const pages = yield* R2.listBuckets.pages({ account_id: "..." });

// Stream all items
const keys = yield* KV.listKeys.items({ account_id: "...", namespace_id: "..." });

Tests

Environment Variables

Variable Required Description
CLOUDFLARE_API_TOKEN Yes API token for authentication
CLOUDFLARE_ACCOUNT_ID Yes Account ID for account-scoped operations
CLOUDFLARE_ZONE_ID No Zone ID for zone-scoped operations (e.g., DNS, ACM read)
CLOUDFLARE_ACM_ZONE_ID No Zone ID for a zone with Advanced Certificate Manager enabled

Running Tests

bun vitest run                              # Run all tests
bun vitest run ./test/services/r2.test.ts   # Run tests for a single service
DEBUG=1 bun vitest run ...                  # Run with request/response debug logs

Zone-Scoped Tests

Some tests require a CLOUDFLARE_ZONE_ID to run. If it is not set, those tests are skipped.

ACM Tests

The ACM (Advanced Certificate Manager) tests use two separate zone IDs:

  • CLOUDFLARE_ZONE_ID — Used for read operations (getTotalTl) and error tests that expect AdvancedCertificateManagerRequired (i.e., a zone without ACM).
  • CLOUDFLARE_ACM_ZONE_ID — Used for write operations (createTotalTl) happy paths that require ACM to be enabled on the zone.

If CLOUDFLARE_ACM_ZONE_ID is not set, the three createTotalTl happy path tests are skipped:

  • happy path - enables Total TLS for a zone
  • happy path - disables Total TLS for a zone
  • happy path - enables Total TLS with certificate authority

Development

bun generate          # Generate from cached spec
bun generate --fetch  # Fetch latest spec and generate
bun test              # Run tests
bun tsc -b            # Type check

License

MIT