cratefield-runtime-cloudflare 0.5.3

Cloudflare Workers runtime for the Cratefield harness (workers-rs)
Documentation
<p align="center">
  <a href="https://github.com/Cratefield/harness">
    <img src="https://raw.githubusercontent.com/Cratefield/harness/main/assets/banners/cratefield-runtime-cloudflare.png" alt="cratefield-runtime-cloudflare — One stateless Worker." width="100%">
  </a>
</p>

<p align="center">
  <a href="https://crates.io/crates/cratefield-runtime-cloudflare"><img src="https://img.shields.io/crates/v/cratefield-runtime-cloudflare.svg?style=flat-square&labelColor=0A0A0B&color=4C6FFF" alt="cratefield-runtime-cloudflare on crates.io"></a>
  <a href="https://docs.rs/cratefield-runtime-cloudflare"><img src="https://img.shields.io/docsrs/cratefield-runtime-cloudflare?style=flat-square&labelColor=0A0A0B&color=EDEBE6" alt="cratefield-runtime-cloudflare documentation"></a>
  <a href="https://github.com/Cratefield/harness/blob/main/LICENSE"><img src="https://img.shields.io/badge/LICENSE-MIT-4C6FFF?style=flat-square&labelColor=0A0A0B" alt="MIT"></a>
</p>

# cratefield-runtime-cloudflare

Cloudflare Workers runtime for the [Cratefield harness](https://github.com/Cratefield/harness):
maps Workers bindings to the harness ports and serves a `Harness` on
`#[event(fetch)]` / `#[event(scheduled)]`.

## Usage

A venture's Worker is three lines:

```rust,ignore
use cratefield_core::Harness;
use cratefield_runtime_cloudflare::{serve, serve_scheduled, Cloudflare};
use std::sync::OnceLock;
use worker::{event, Context, Env, Request, Response};

static INSTANCE: OnceLock<(Harness, Cloudflare)> = OnceLock::new();

#[event(fetch)]
pub async fn fetch(req: Request, env: Env, ctx: Context) -> worker::Result<Response> {
    let (harness, runtime) = INSTANCE.get_or_init(build);
    serve(harness, runtime, req, env, ctx).await
}

#[event(scheduled)]
pub async fn scheduled(event: worker::ScheduledEvent, env: Env, ctx: worker::ScheduleContext) {
    let (harness, runtime) = INSTANCE.get_or_init(build);
    serve_scheduled(harness, runtime, event, env, ctx).await;
}
```

The runtime builder: `Cloudflare::new().db("DB").kv("KV").rate_limiter("RATE_LIMITER")`
plus `.mailer(...)`/`.captcha(...)` with adapter instances. For per-key
limits (per plan, per address — the binding above is one limit per
namespace), wire `.d1_rate_limiter("DB", policy)` instead: a D1-backed
fixed window per key whose policy maps each key to its cap. Ship the
table it counts in by adding the `RATE_LIMIT_COUNTERS_SQL` statement to
the venture's `migrations/` directory.

## wrangler.toml

```toml
name = "my-venture-api"
main = "build/worker/shim.mjs"
compatibility_date = "2026-09-01"

[build]
command = "cargo install -q worker-build && worker-build --release"

[observability]
enabled = true

[[d1_databases]]
binding = "DB"
database_name = "my-venture-db"
database_id = "<uuid>"
migrations_dir = "migrations"

# Optional bindings:
# [kv_namespaces] binding = "KV", id = "..."
# [unsafe.bindings] name = "RATE_LIMITER", type = "ratelimit"
```

Secrets (`wrangler secret put`, or `.dev.vars` locally — copy `.dev.vars.example`):
`HARNESS_SECRET` (required for the Signer port, ≥ 32 bytes),
`HARNESS_SECRET_PREVIOUS` (rotation), `ADMIN_TOKEN`, `ENV`.

## wasm notes (workerd/miniflare, wrangler 4.x)

These are empirical facts recorded while building this crate; see
`PROGRESS.md` issue #5 for the full debugging history:

- The `worker` crate's `http` feature stays **off**: with it enabled, D1
  writes hang the isolate. `serve()` therefore takes the native
  `worker::Request` (the fetch macro's `FromRequest` accepts it).
- Request bodies are gated before they are resident (issue #440): a
  `content-length` over the route's ceiling is refused unread, and a body
  with no usable `content-length` is read through `Request::stream()` and
  cut off once the ceiling is passed. A request with no body at all (most
  GETs) is answered as empty without any read: 0.8.5's `stream()` sets
  `body_used` before discovering there is no body, after which `bytes()`
  only answers `BodyUsed`. Declared lengths within the ceiling
  are still buffered via `Request::bytes()`; every streaming bridge
  between `worker::Body` and axum hangs.
- A route a module declares in `streaming_routes()` is served opt-in in
  streaming mode (issue #585). The request half turns `Request::stream()`
  into a `RequestStream` (a `!Send` worker `ByteStream` behind
  `worker::send::SendWrapper`, sound because an isolate is single-threaded
  — ADR 0002) and hands the router an empty body plus the handle, so
  nothing is buffered; a `content-length` over the route's own ceiling is
  still refused unread. The response half takes the `ResponseStream` a
  handler left in the response extensions and bridges it with
  `worker::Response::from_stream` — never `to_bytes`, which is capped at
  1 MiB and for a stream would be the hang above. Every non-streaming
  response is still buffered under that 1 MiB ceiling.
- `wrangler dev` only: refusing a body unread (issue #440) leaves the
  tail undrained, and the dev proxy's `middleware-ensure-req-body-drained`
  logs `Failed to drain the unused request body. Error: Network connection
  lost.` and breaks miniflare's `ProxyWorker` loopback slot, so the NEXT
  local request fails with a spurious 500 `Network connection lost` — it
  never reaches the isolate. Exactly one request is lost and the dev
  server recovers by itself; a client that overstates `content-length`
  and then sends fewer bytes may see the 413 withheld locally for the
  same reason. Dev-proxy artefact only: production workerd cancels
  unread request bodies natively (no `ProxyWorker`, no drain middleware).
  Verified wrangler 4.135.0 / workerd 1.20260918.1; known upstream as
  workers-sdk issue #4562.
- Installing a `tracing` dispatcher (`set_global_default`/`set_default`)
  hangs the isolate, so `install_tracing()` is a no-op on wasm and runtime
  logs go through `worker::console_log!`/`console_error!` directly
  (`rt_log!`). The JSON-lines subscriber with field redaction is used on
  native runs.
- The `time` crate needs its `wasm-bindgen` feature for `now_utc()` on
  wasm (pinned in the workspace `Cargo.toml`); without it every clock read
  panics (`time not implemented on this platform`).

---

MIT. Built in the open for [Cratefield](https://cratefield.com), a [Factory Zero](https://factory0.ventures) venture.