@ontrails/cloudflare 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,160 @@
1
+ # @ontrails/cloudflare
2
+
3
+ ## 0.2.0
4
+
5
+ ### Patch Changes
6
+
7
+ - [`b3ddf91`](https://github.com/outfitter-dev/trails/commit/b3ddf918ada8211f44983512e2bbdfbc5b66d722): Prepare the first normal Trails release at `0.2.0` on `latest`, replacing the unpublished 1.0.0 source release. The target advances the original `0.1.0` source minor, which was never published under the current package names. Public packages remain in lockstep. Update consumer installation guidance and provide a temporary manifest bridge for old 1.0 beta sources. Published beta versions remain unchanged; minor 0.x releases may carry documented breaking changes.
8
+
9
+ The `1.0.0` section retained below records an unpublished preparation, not an npm release. Its accumulated changes are included in `0.2.0`; the section remains as source history alongside the published beta entries.
10
+
11
+ ## 1.0.0
12
+
13
+ ### Minor Changes
14
+
15
+ - [`9874e0b`](https://github.com/outfitter-dev/trails/commit/9874e0bb034c0f98edeb19833d9d3519c2a07a4c): Add `@ontrails/cloudflare/d1`, an env-bound Cloudflare D1 store resource for `@ontrails/store` definitions. The new subpath exports `cloudflareD1` and `connectD1`, supports the backend-agnostic store accessor contract (`get`, `list`, `upsert`, `remove`), versioned-table optimistic concurrency, fixture/mock seeding, store-derived write signals, Miniflare-backed conformance tests, and Worker env-bridge integration.
16
+
17
+ `@ontrails/core` and `@ontrails/store` no longer require the Bun global for signal fire ids or late-bound store signal tokens, so store definitions and store-derived signal emission work inside Worker modules. `@ontrails/warden` now treats `cloudflareD1` as a required Cloudflare public export with `@example` coverage.
18
+
19
+ - [`1e64ee7`](https://github.com/outfitter-dev/trails/commit/1e64ee7bc270901486c5bb51ac38bf045c924adc): Add first-class queue activation sources with `queue()` in `@ontrails/core`.
20
+ Queue sources validate their runtime queue name and parse contract, project the
21
+ queue name into durable topo facts, participate in activation input
22
+ compatibility, and block established outputs when malformed.
23
+
24
+ Add `@ontrails/cloudflare/queues` with `cloudflareQueue`, `createMemoryQueue`,
25
+ and `createQueueHandler`. Cloudflare Workers now expose both `fetch` and
26
+ `queue` entrypoints from `createWorkersHandler`, resolve env-bound resources for
27
+ queue-activated trails, acknowledge successful/skipped/cancelled messages, and
28
+ acknowledge traced non-retryable Trails errors so permanently invalid messages
29
+ do not churn through the queue. Failures explicitly marked retryable enter
30
+ Cloudflare's retry and DLQ flow, with rate-limit delays preserved.
31
+
32
+ `@ontrails/warden` now treats queue activation sources as materialized and
33
+ requires `cloudflareQueue` public export example coverage.
34
+
35
+ - [`4086b5b`](https://github.com/outfitter-dev/trails/commit/4086b5b2f01b24660924fd8b667523f38caaed29): Add `@ontrails/cloudflare/r2`, an env-bound Cloudflare R2 bucket resource with
36
+ `cloudflareR2`, `createMemoryR2`, and `r2ObjectToBlobRef`. The resource
37
+ materializes Worker `r2_buckets` bindings through the shared env bridge, records
38
+ Cloudflare lock overlay facts, carries an in-memory object mock for
39
+ configuration-free tests, and documents the supported object operations plus
40
+ streaming/metadata boundaries.
41
+
42
+ `@ontrails/warden` now treats `cloudflareR2` as a required Cloudflare public
43
+ export with `@example` coverage.
44
+
45
+ - [`a105127`](https://github.com/outfitter-dev/trails/commit/a105127e5662ed9a6c245125f791fb0182da3f5e): Add the `@ontrails/cloudflare` adapter collection with its first two service subpaths. `@ontrails/cloudflare/workers` exports `createWorkersHandler`, a materializer producing the `{ fetch(request, env, ctx) }` Worker export on the shared HTTP fetch kernel, with an env bridge that re-resolves env-bound resources whenever a new Worker `env` arrives so no resource instance serves a request with a stale env. `@ontrails/cloudflare/kv` exports `cloudflareKv`, a resource definition wrapping a KV namespace binding (`get`/`put`/`delete`/`list` with TTL options) plus an in-memory `createMemoryKv` mock so `testAll` runs configuration-free.
46
+
47
+ `@ontrails/core` now guards the default trail context fields: `requestId` falls back to `crypto.randomUUID()` when the `Bun` global is absent, and `cwd`/`env` fall back to `'/'`/`{}` when `process` is absent, so trail execution works on runtimes like Cloudflare Workers.
48
+
49
+ `@ontrails/warden` registers the `@ontrails/cloudflare` public barrel in the repo-local `public-export-example-coverage` policy, requiring `@example` TSDoc coverage on `createWorkersHandler` and `cloudflareKv`.
50
+
51
+ - [`5adb995`](https://github.com/outfitter-dev/trails/commit/5adb99551c2dda6190d46cce7f60bb08d63c99aa): Complete the v1 hard cutover from the authored `blaze` field to
52
+ `implementation` across trail contracts, surface projections, tests, examples,
53
+ and public source-analysis helpers. Existing applications must rename authored
54
+ trail behavior fields and direct trail-object access before upgrading.
55
+ - [`88a6a62`](https://github.com/outfitter-dev/trails/commit/88a6a62a9e9e230ca6d368fa78dc3ece6c816204): Complete the v1 classification-first cutover from projection/project vocabulary
56
+ to derive/derived for contract-owned fact production and render/rendered for
57
+ surface presentation. Public type, helper, rule, relation, and report names move
58
+ without compatibility aliases; ordinary repository/project nouns remain
59
+ explicit preserves or structured review inventory.
60
+ - [`cc169e2`](https://github.com/outfitter-dev/trails/commit/cc169e2a9b580036b0c6e4ce77d396db6a34f830): Add `cloudflareOverlay`, the first lock overlay overlay: it derives the app's env-bound resources into `overlays.cloudflare` (wrangler binding name per resource) when the app exports it via `trailsOverlays` and runs `trails compile`.
61
+
62
+ ### Patch Changes
63
+
64
+ - [`6b75a46`](https://github.com/outfitter-dev/trails/commit/6b75a46ab6210237d306cceade833bf9ce6e7431): The core barrel is now execution-portable: no eager `bun:`/`node:` builtin imports remain on its module graph (TRL-1198). `trails-db`, workspace discovery, and path security load `bun:sqlite`, `node:fs`, `node:os`, and `node:path` lazily through `process.getBuiltinModule` at first use, and signal payload summaries plus per-project store keys use a pure SHA-256 (output-identical to `node:crypto`). A Worker bundle no longer needs a `bun:sqlite` stub plugin or the `nodejs_compat` flag to serve trails; the Cloudflare adapter's miniflare lane now bundles without externals and boots workerd without `nodejs_compat` as the structural regression gate, and its README stub instructions are replaced with the portable posture. Tooling helpers throw a clear `InternalError` naming the missing builtin when called on runtimes without it.
65
+ - [`b1fbe57`](https://github.com/outfitter-dev/trails/commit/b1fbe574e6f44d1fecb5e3a000270955c0a77b7b): Publish Bun-validated package tarballs through an npm trusted-publishing adapter
66
+ binding, add exact repository metadata for each public workspace package, and
67
+ correct the native Bun release descriptor to its pack-only runtime boundary.
68
+
69
+ ## 1.0.0-beta.50
70
+
71
+ ## 1.0.0-beta.49
72
+
73
+ ## 1.0.0-beta.48
74
+
75
+ ## 1.0.0-beta.47
76
+
77
+ ## 1.0.0-beta.46
78
+
79
+ ## 1.0.0-beta.45
80
+
81
+ ## 1.0.0-beta.44
82
+
83
+ ### Patch Changes
84
+
85
+ - [`b1fbe57`](https://github.com/outfitter-dev/trails/commit/b1fbe574e6f44d1fecb5e3a000270955c0a77b7b): Publish Bun-validated package tarballs through an npm trusted-publishing adapter
86
+ binding, add exact repository metadata for each public workspace package, and
87
+ correct the native Bun release descriptor to its pack-only runtime boundary.
88
+
89
+ ## 1.0.0-beta.43
90
+
91
+ ### Minor Changes
92
+
93
+ - [`88a6a62`](https://github.com/outfitter-dev/trails/commit/88a6a62a9e9e230ca6d368fa78dc3ece6c816204): Complete the v1 classification-first cutover from projection/project vocabulary
94
+ to derive/derived for contract-owned fact production and render/rendered for
95
+ surface presentation. Public type, helper, rule, relation, and report names move
96
+ without compatibility aliases; ordinary repository/project nouns remain
97
+ explicit preserves or structured review inventory.
98
+
99
+ ## 1.0.0-beta.42
100
+
101
+ ## 1.0.0-beta.41
102
+
103
+ ## 1.0.0-beta.40
104
+
105
+ ### Minor Changes
106
+
107
+ - [`9874e0b`](https://github.com/outfitter-dev/trails/commit/9874e0bb034c0f98edeb19833d9d3519c2a07a4c): Add `@ontrails/cloudflare/d1`, an env-bound Cloudflare D1 store resource for `@ontrails/store` definitions. The new subpath exports `cloudflareD1` and `connectD1`, supports the backend-agnostic store accessor contract (`get`, `list`, `upsert`, `remove`), versioned-table optimistic concurrency, fixture/mock seeding, store-derived write signals, Miniflare-backed conformance tests, and Worker env-bridge integration.
108
+
109
+ `@ontrails/core` and `@ontrails/store` no longer require the Bun global for signal fire ids or late-bound store signal tokens, so store definitions and store-derived signal emission work inside Worker modules. `@ontrails/warden` now treats `cloudflareD1` as a required Cloudflare public export with `@example` coverage.
110
+
111
+ - [`1e64ee7`](https://github.com/outfitter-dev/trails/commit/1e64ee7bc270901486c5bb51ac38bf045c924adc): Add first-class queue activation sources with `queue()` in `@ontrails/core`.
112
+ Queue sources validate their runtime queue name and parse contract, project the
113
+ queue name into durable topo facts, participate in activation input
114
+ compatibility, and block established outputs when malformed.
115
+
116
+ Add `@ontrails/cloudflare/queues` with `cloudflareQueue`, `createMemoryQueue`,
117
+ and `createQueueHandler`. Cloudflare Workers now expose both `fetch` and
118
+ `queue` entrypoints from `createWorkersHandler`, resolve env-bound resources for
119
+ queue-activated trails, acknowledge successful/skipped/cancelled messages, and
120
+ acknowledge traced non-retryable Trails errors so permanently invalid messages
121
+ do not churn through the queue. Failures explicitly marked retryable enter
122
+ Cloudflare's retry and DLQ flow, with rate-limit delays preserved.
123
+
124
+ `@ontrails/warden` now treats queue activation sources as materialized and
125
+ requires `cloudflareQueue` public export example coverage.
126
+
127
+ - [`4086b5b`](https://github.com/outfitter-dev/trails/commit/4086b5b2f01b24660924fd8b667523f38caaed29): Add `@ontrails/cloudflare/r2`, an env-bound Cloudflare R2 bucket resource with
128
+ `cloudflareR2`, `createMemoryR2`, and `r2ObjectToBlobRef`. The resource
129
+ materializes Worker `r2_buckets` bindings through the shared env bridge, records
130
+ Cloudflare lock overlay facts, carries an in-memory object mock for
131
+ configuration-free tests, and documents the supported object operations plus
132
+ streaming/metadata boundaries.
133
+
134
+ `@ontrails/warden` now treats `cloudflareR2` as a required Cloudflare public
135
+ export with `@example` coverage.
136
+
137
+ - [`5adb995`](https://github.com/outfitter-dev/trails/commit/5adb99551c2dda6190d46cce7f60bb08d63c99aa): Complete the v1 hard cutover from the authored `blaze` field to
138
+ `implementation` across trail contracts, surface projections, tests, examples,
139
+ and public source-analysis helpers. Existing applications must rename authored
140
+ trail behavior fields and direct trail-object access before upgrading.
141
+
142
+ ## 1.0.0-beta.39
143
+
144
+ ### Minor Changes
145
+
146
+ - [`cc169e2`](https://github.com/outfitter-dev/trails/commit/cc169e2a9b580036b0c6e4ce77d396db6a34f830): Add `cloudflareOverlay`, the first lock overlay overlay: it derives the app's env-bound resources into `overlays.cloudflare` (wrangler binding name per resource) when the app exports it via `trailsOverlays` and runs `trails compile`.
147
+
148
+ ### Patch Changes
149
+
150
+ - [`6b75a46`](https://github.com/outfitter-dev/trails/commit/6b75a46ab6210237d306cceade833bf9ce6e7431): The core barrel is now execution-portable: no eager `bun:`/`node:` builtin imports remain on its module graph (TRL-1198). `trails-db`, workspace discovery, and path security load `bun:sqlite`, `node:fs`, `node:os`, and `node:path` lazily through `process.getBuiltinModule` at first use, and signal payload summaries plus per-project store keys use a pure SHA-256 (output-identical to `node:crypto`). A Worker bundle no longer needs a `bun:sqlite` stub plugin or the `nodejs_compat` flag to serve trails; the Cloudflare adapter's miniflare lane now bundles without externals and boots workerd without `nodejs_compat` as the structural regression gate, and its README stub instructions are replaced with the portable posture. Tooling helpers throw a clear `InternalError` naming the missing builtin when called on runtimes without it.
151
+
152
+ ## 1.0.0-beta.38
153
+
154
+ ### Minor Changes
155
+
156
+ - [`a105127`](https://github.com/outfitter-dev/trails/commit/a105127e5662ed9a6c245125f791fb0182da3f5e): Add the `@ontrails/cloudflare` adapter collection with its first two service subpaths. `@ontrails/cloudflare/workers` exports `createWorkersHandler`, a materializer producing the `{ fetch(request, env, ctx) }` Worker export on the shared HTTP fetch kernel, with an env bridge that re-resolves env-bound resources whenever a new Worker `env` arrives so no resource instance serves a request with a stale env. `@ontrails/cloudflare/kv` exports `cloudflareKv`, a resource definition wrapping a KV namespace binding (`get`/`put`/`delete`/`list` with TTL options) plus an in-memory `createMemoryKv` mock so `testAll` runs configuration-free.
157
+
158
+ `@ontrails/core` now guards the default trail context fields: `requestId` falls back to `crypto.randomUUID()` when the `Bun` global is absent, and `cwd`/`env` fall back to `'/'`/`{}` when `process` is absent, so trail execution works on runtimes like Cloudflare Workers.
159
+
160
+ `@ontrails/warden` registers the `@ontrails/cloudflare` public barrel in the repo-local `public-export-example-coverage` policy, requiring `@example` TSDoc coverage on `createWorkersHandler` and `cloudflareKv`.
package/README.md ADDED
@@ -0,0 +1,269 @@
1
+ # @ontrails/cloudflare
2
+
3
+ The Cloudflare adapter collection for Trails. One package, one subpath per Cloudflare service, each connecting a service to the Trails primitive it naturally serves:
4
+
5
+ | Subpath | Serves | Status |
6
+ | --- | --- | --- |
7
+ | `@ontrails/cloudflare/workers` | HTTP surface materializer (fetch handler) | ✅ |
8
+ | `@ontrails/cloudflare/kv` | Key-value resource | ✅ |
9
+ | `@ontrails/cloudflare/d1` | D1-backed store resource | ✅ |
10
+ | `@ontrails/cloudflare/queues` | Queue producer resource + consumer materializer | ✅ |
11
+ | `@ontrails/cloudflare/r2` | R2 blob/object resource | ✅ |
12
+
13
+ Adapter composition doctrine applies throughout: subpaths take primitive-authored declarations (a resource definition, a surface config) and never shadow authoring verbs. Bindings arrive ambiently on the Worker `env`, so runtime dependencies are near-zero.
14
+
15
+ ## `/workers` — the Worker materializer
16
+
17
+ `createWorkersHandler(graph, options)` produces the Worker export for Cloudflare runtime entrypoints. Its `fetch(request, env, ctx)` member delegates to the shared HTTP fetch kernel from `@ontrails/http` — the same kernel behind the Bun and Hono surfaces, so routes, validation, error rendering, and webhook handling behave identically. Its `queue(batch, env, ctx)` member dispatches first-class core `queue()` activation sources through the `/queues` materializer.
18
+
19
+ ```ts
20
+ // src/worker.ts
21
+ import { createWorkersHandler } from '@ontrails/cloudflare/workers';
22
+ import { graph } from './app.js';
23
+
24
+ export default createWorkersHandler(graph, { basePath: '/api' });
25
+ ```
26
+
27
+ Options mirror the other HTTP surfaces: `basePath`, `createContext`, `layers`, `maxJsonBodyBytes`, `resolvePermit`, plus include/exclude/intent filtering. `resources` accepts either a static override map or a function of the Worker env:
28
+
29
+ ```ts
30
+ export default createWorkersHandler(graph, {
31
+ resources: (env) => ({ audit: createAuditClient(env['AUDIT_URL']) }),
32
+ });
33
+ ```
34
+
35
+ ### The env bridge
36
+
37
+ Worker bindings (KV, D1, R2, queues) live on `env`, which arrives per request — they cannot be captured at module init. The bridge closes that gap once, for every subpath in this collection:
38
+
39
+ 1. A subpath authors an ordinary `resource()` definition and registers an `EnvBindingSpec` for it (`registerEnvBinding(definition, { binding, fromEnv })`).
40
+ 2. `createWorkersHandler` walks the declared resources of the trails the surface actually exposes (honoring `include`/`exclude`/`intent`, and including fork-version resources), and for each env-bound definition resolves `env[binding]` through `fromEnv` into a resource override. Explicitly overridden resource IDs skip env resolution entirely, so an override never requires its binding.
41
+ 3. The kernel handler is materialized per env identity. The Workers runtime keeps `env` stable within an isolate, so steady-state requests reuse one materialization — but any request carrying a different env object re-resolves every env-bound resource before it executes.
42
+
43
+ Because resource overrides are checked before core's singleton resource cache, no resource instance can serve a request with a stale env. This guarantee has a dedicated regression test (`src/workers/__tests__/env-bridge.test.ts`).
44
+
45
+ Missing or mistyped bindings fail the request with a redacted 500 and log full diagnostics to the Worker log, naming the binding and the resource that needed it.
46
+
47
+ ### Runtime notes
48
+
49
+ - The core execution path is runtime-portable (TRL-1198): `@ontrails/core` loads `bun:sqlite` and `node:` builtins lazily at first use, so a Worker bundle needs no stub plugin and no `nodejs_compat` flag to serve trails. The integration lane (`src/__tests__/miniflare.test.ts`) bundles the demo Worker with no externals and boots workerd without `nodejs_compat` as the structural regression gate.
50
+ - Tooling helpers on the core barrel (the trails-db store, workspace discovery) still require a Bun or Node runtime when actually called; on workerd they throw a clear `InternalError` naming the missing builtin instead of poisoning the module graph.
51
+ - Explicit `resources` overrides win over env-bound resolution, which is how tests substitute fakes.
52
+
53
+ ## `/kv` — the key-value resource
54
+
55
+ `cloudflareKv(id, { binding })` authors a resource wrapping a KV namespace binding. Trails declare it with `resources: [...]` and read it with `flags.from(ctx)` — the standard accessor pattern.
56
+
57
+ ```ts
58
+ import { cloudflareKv } from '@ontrails/cloudflare/kv';
59
+ import { trail, Result } from '@ontrails/core';
60
+ import { z } from 'zod';
61
+
62
+ const flags = cloudflareKv('flags', { binding: 'FLAGS' });
63
+
64
+ const showFlag = trail('flag.show', {
65
+ implementation: async (input, ctx) => {
66
+ const value = await flags.from(ctx).get(input.key);
67
+ return Result.ok({ value });
68
+ },
69
+ input: z.object({ key: z.string() }),
70
+ intent: 'read',
71
+ output: z.object({ value: z.string().nullable() }),
72
+ resources: [flags],
73
+ });
74
+ ```
75
+
76
+ The client surface is `get`/`put`/`delete`/`list`, with TTL options on `put` (`expirationTtl` in seconds, or an absolute `expiration` Unix timestamp) and prefix/limit/cursor pagination on `list`. A real `KVNamespace` binding satisfies the shape structurally, so the env bridge passes it through unchanged.
77
+
78
+ Declare the binding in wrangler config:
79
+
80
+ ```toml
81
+ kv_namespaces = [
82
+ { binding = "FLAGS", id = "<namespace-id>" }
83
+ ]
84
+ ```
85
+
86
+ ### Testing with the mock
87
+
88
+ Every `cloudflareKv` resource carries an in-memory mock factory, so `testAll(app)` runs configuration-free — no Cloudflare account, no wrangler:
89
+
90
+ ```ts
91
+ import { testAll } from '@ontrails/testing';
92
+ import { graph } from '../src/app.js';
93
+
94
+ testAll(graph);
95
+ ```
96
+
97
+ `createMemoryKv()` is also exported directly for hand-rolled tests, with an injectable clock for TTL assertions. Two documented divergences from the real binding: the mock does not enforce KV's 60-second minimum TTL, and when both `expiration` and `expirationTtl` are passed the mock prefers `expirationTtl` where the real binding rejects the combination.
98
+
99
+ ## `/d1` — the store resource
100
+
101
+ `cloudflareD1(definition, { binding, id })` binds an `@ontrails/store` definition to a Cloudflare D1 database. Trails declare the returned resource and use the standard store accessors (`get`, `list`, `upsert`, `remove`) through `db.from(ctx)`.
102
+
103
+ ```ts
104
+ import { cloudflareD1 } from '@ontrails/cloudflare/d1';
105
+ import { trail, Result } from '@ontrails/core';
106
+ import { store } from '@ontrails/store';
107
+ import { z } from 'zod';
108
+
109
+ const definition = store({
110
+ notes: {
111
+ identity: 'id',
112
+ schema: z.object({ id: z.string(), body: z.string() }),
113
+ },
114
+ });
115
+
116
+ const db = cloudflareD1(definition, { binding: 'DB', id: 'notes.store' });
117
+
118
+ const saveNote = trail('note.save', {
119
+ implementation: async (input, ctx) =>
120
+ Result.ok(await db.from(ctx).notes.upsert(input)),
121
+ input: z.object({ id: z.string(), body: z.string() }),
122
+ intent: 'write',
123
+ output: z.object({ id: z.string(), body: z.string() }),
124
+ resources: [db],
125
+ });
126
+ ```
127
+
128
+ Declare the binding in wrangler config:
129
+
130
+ ```toml
131
+ d1_databases = [
132
+ { binding = "DB", database_name = "my-worker", database_id = "<database-id>" }
133
+ ]
134
+ ```
135
+
136
+ The first implementation stores one JSON entity per D1 row (`id TEXT PRIMARY KEY`, `entity TEXT NOT NULL`, nullable `version INTEGER`) in adapter-owned tables prefixed by the resource id. It preserves the backend-agnostic store contract, including generated identity fields, `createdAt`/`updatedAt` generation, versioned-table optimistic concurrency (`ConflictError` on stale `version`), fixture/mock seeding, and store-derived `created`/`updated`/`removed` signals.
137
+
138
+ Capability boundaries are explicit:
139
+
140
+ - `indexed`/`indexes`, `references`, and `search` stay store metadata for this driver; the D1 adapter does not create secondary indexes, foreign keys, or FTS tables yet.
141
+ - `list(filters)` performs simple equality filtering in the adapter after reading table rows, then applies `offset`/`limit`.
142
+ - `connectD1(definition, database, options)` is exported for tests and advanced runtimes that already hold a D1 binding. Schema creation and optional runtime `seed` run lazily before the first accessor call so the Workers env bridge can resolve resources synchronously. Runtime seed rows require explicit stable identities and are insert-only, so rematerializing a Worker cannot overwrite user edits.
143
+
144
+ Every `cloudflareD1` resource carries an in-memory mock factory seeded from table fixtures or `mockSeed`, so `testAll(app)` remains configuration-free. Miniflare can provide a real D1 binding for local integration tests without a Cloudflare account.
145
+
146
+ ## `/r2` — blob/object resource
147
+
148
+ `cloudflareR2(id, { binding })` authors a resource wrapping a Cloudflare R2 bucket binding. Trails declare it with `resources: [...]` and use `bucket.from(ctx)` to call the Worker binding's object operations (`put`, `get`, `head`, `delete`, and `list`).
149
+
150
+ ```ts
151
+ import {
152
+ NotFoundError,
153
+ Result,
154
+ blobRefSchema,
155
+ trail,
156
+ } from '@ontrails/core';
157
+ import { cloudflareR2, r2ObjectToBlobRef } from '@ontrails/cloudflare/r2';
158
+ import { z } from 'zod';
159
+
160
+ const assets = cloudflareR2('assets', { binding: 'ASSETS' });
161
+
162
+ const readAsset = trail('asset.read', {
163
+ implementation: async (input, ctx) => {
164
+ const object = await assets.from(ctx).get(input.key);
165
+ if (object === null || !('body' in object)) {
166
+ return Result.err(new NotFoundError(`Asset "${input.key}" not found`));
167
+ }
168
+ return Result.ok(r2ObjectToBlobRef(object));
169
+ },
170
+ input: z.object({ key: z.string() }),
171
+ intent: 'read',
172
+ output: blobRefSchema,
173
+ resources: [assets],
174
+ });
175
+ ```
176
+
177
+ Declare the binding in wrangler config:
178
+
179
+ ```toml
180
+ [[r2_buckets]]
181
+ binding = "ASSETS"
182
+ bucket_name = "my-assets"
183
+ ```
184
+
185
+ The resource surface follows the R2 Worker binding structurally. A real R2 bucket binding passes through unchanged, including Cloudflare's conditional operation behavior where `get()` can return metadata without a body and `put()` can return `null` when a precondition fails. `r2ObjectToBlobRef(object)` is the small bridge from a fetched R2 object body to core's `BlobRef` binary-output contract; the HTTP and MCP surfaces already know how to render `blobRefSchema`.
186
+
187
+ Capability boundaries are explicit:
188
+
189
+ - The adapter does not expose public bucket URLs, signed URLs, S3 clients, multipart upload helpers, or object-event subscriptions.
190
+ - The in-memory mock implements object bytes, metadata, `put`, `get`, `head`, `delete`, and lexicographic `list` pagination with prefix/cursor/delimiter grouping. It accepts SSE-C option shapes for binding compatibility but does not encrypt objects, validate keys, or emit `ssecKeyMd5`; it also does not model R2 preconditions, range reads, checksums, storage-tier billing behavior, or multipart uploads.
191
+ - For raw HTTP upload/download routes, keep authorization in your own trails or surface layer; the R2 resource only materializes the bucket binding.
192
+
193
+ Every `cloudflareR2` resource carries an in-memory mock (`createMemoryR2`) so object trails work in `testAll(app)` and focused unit tests without a Cloudflare account or wrangler.
194
+
195
+ ## `/queues` — Queue producer and consumer support
196
+
197
+ `cloudflareQueue(id, { binding })` authors a resource wrapping a Cloudflare Queue producer binding. Trails declare it with `resources: [...]` and send messages with `jobs.from(ctx).send(...)` or `sendBatch(...)`.
198
+
199
+ ```ts
200
+ import { cloudflareQueue } from '@ontrails/cloudflare/queues';
201
+ import { Result, queue, trail } from '@ontrails/core';
202
+ import { z } from 'zod';
203
+
204
+ const jobs = cloudflareQueue<{ id: string }>('jobs', { binding: 'JOBS' });
205
+
206
+ const enqueueJob = trail('job.enqueue', {
207
+ implementation: async (input, ctx) => {
208
+ await jobs.from(ctx).send({ id: input.id });
209
+ return Result.ok({ queued: true });
210
+ },
211
+ input: z.object({ id: z.string() }),
212
+ output: z.object({ queued: z.boolean() }),
213
+ resources: [jobs],
214
+ });
215
+ ```
216
+
217
+ Queue consumers are authored with the core `queue()` activation source and materialized by `createWorkersHandler`:
218
+
219
+ ```ts
220
+ const consumeJob = trail('job.consume', {
221
+ implementation: async (input) => Result.ok({ processed: input.id }),
222
+ input: z.object({ id: z.string() }),
223
+ on: [
224
+ queue('queue.jobs', {
225
+ queue: 'jobs',
226
+ parse: z.object({ id: z.string() }),
227
+ }),
228
+ ],
229
+ output: z.object({ processed: z.string() }),
230
+ });
231
+ ```
232
+
233
+ Declare the producer binding and consumer in wrangler config:
234
+
235
+ ```toml
236
+ [[queues.producers]]
237
+ binding = "JOBS"
238
+ queue = "jobs"
239
+
240
+ [[queues.consumers]]
241
+ queue = "jobs"
242
+ max_batch_size = 10
243
+ max_retries = 3
244
+ dead_letter_queue = "jobs-dlq"
245
+ ```
246
+
247
+ The consumer materializer acknowledges each message after all matching Trails queue consumers succeed, skip by `where`, or return `CancelledError`. It also traces and acknowledges non-retryable Trails errors, including validation failures, so permanently invalid messages do not churn through the queue. Errors explicitly marked retryable call `message.retry(...)`; Cloudflare's retry and dead-letter configuration decides when a retried message moves to a DLQ. `RateLimitError.retryAfter` is passed through as `delaySeconds`.
248
+
249
+ Every `cloudflareQueue` resource carries an in-memory mock (`createMemoryQueue`) so producer trails work in `testAll(app)` and unit tests can inspect sent messages. The local Worker-env regression test exercises a queue-activated trail using KV through the env bridge; the Miniflare lane remains focused on HTTP/webhook/KV/D1 until its queue harness is wired in this repo.
250
+
251
+ ## Local integration testing
252
+
253
+ Integration runs are local-first via [miniflare](https://miniflare.dev) (workerd in-process): the test lane bundles a demo Worker with `Bun.build`, boots it with real KV and D1 bindings, and exercises HTTP, webhook, KV, and D1 store routes. Queue producer/consumer behavior and R2 object behavior are covered by focused structural tests under `src/queues/__tests__/queues.test.ts` and `src/r2/__tests__/r2.test.ts`. Real-account deploys are manual and never CI-required.
254
+
255
+ ## Lock facts
256
+
257
+ `cloudflareOverlay` (root export) is the adapter's lock overlay overlay: an `Overlay` pairing the `cloudflare` namespace with an elevated zod fact schema and a deterministic derive over the app's topo. It records every env-bound resource as `{ binding, resourceId }` so the committed `trails.lock` documents which wrangler bindings the app depends on.
258
+
259
+ An app opts in by exporting the overlay list next to its topo, then compiling:
260
+
261
+ ```ts
262
+ // src/app.ts
263
+ import { cloudflareOverlay } from '@ontrails/cloudflare';
264
+
265
+ export const app = topo('my-worker', { readFlag });
266
+ export const trailsOverlays = [cloudflareOverlay];
267
+ ```
268
+
269
+ `trails compile` validates the derived facts against the schema and embeds them as `overlays.cloudflare`; `trails wayfind --overlay cloudflare` reads them back. Toolchains that predate an overlay's namespace preserve it byte-for-byte — adding a new fact family never edits the lock schema or graph type.
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@ontrails/cloudflare",
3
+ "version": "0.2.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/outfitter-dev/trails.git",
7
+ "directory": "adapters/cloudflare"
8
+ },
9
+ "files": [
10
+ "src/**/*.ts",
11
+ "!src/**/__tests__/**",
12
+ "!src/**/*.test.ts",
13
+ "!src/**/*.test-d.ts",
14
+ "README.md",
15
+ "CHANGELOG.md"
16
+ ],
17
+ "type": "module",
18
+ "exports": {
19
+ ".": "./src/index.ts",
20
+ "./workers": "./src/workers/index.ts",
21
+ "./kv": "./src/kv/index.ts",
22
+ "./d1": "./src/d1/index.ts",
23
+ "./r2": "./src/r2/index.ts",
24
+ "./queues": "./src/queues/index.ts",
25
+ "./package.json": "./package.json"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -b",
29
+ "test": "bun test --max-concurrency=1",
30
+ "typecheck": "tsc --noEmit",
31
+ "lint": "oxlint ./src",
32
+ "clean": "rm -rf dist *.tsbuildinfo"
33
+ },
34
+ "dependencies": {
35
+ "@ontrails/core": "^0.2.0"
36
+ },
37
+ "devDependencies": {
38
+ "@ontrails/adapter-kit": "^0.2.0",
39
+ "@ontrails/testing": "^0.2.0",
40
+ "miniflare": "^4.20250617.4"
41
+ },
42
+ "peerDependencies": {
43
+ "@ontrails/http": "^0.2.0",
44
+ "@ontrails/store": "^0.2.0",
45
+ "zod": "^4.3.5"
46
+ },
47
+ "trails": {
48
+ "adapter": {
49
+ "target": "http"
50
+ },
51
+ "adapters": {
52
+ "./d1": {
53
+ "target": "store"
54
+ }
55
+ }
56
+ }
57
+ }