diff --git a/docs/architecture.md b/docs/architecture.md index 0ca791b..9e4df17 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -9,7 +9,7 @@ Anything not locked here is a Stage-2 implementation choice. | :--- | :--- | :--- | :--- | | Ingester + window aggregator | Python / `cachekit` | `0.15.0` (PyPI) | Render free web service | | Edge API | TypeScript / `@cachekit-io/cachekit` | `0.1.3` (npm) | Cloudflare Workers (free plan) | -| Edge hot path | Rust / `cachekit-rs` | `0.5.0` (crates.io) | Cloudflare Workers, `wasm32-unknown-unknown` | +| Edge hot path | Rust / `cachekit-rs` | `0.7.0` (crates.io) | Cloudflare Workers, `wasm32-unknown-unknown` | | Dashboard | static HTML/JS | — | Cloudflare Workers Assets | | Cache backend | CachekitIO | `api.dev.cachekit.io` | ours (dogfood) | | Data source | Bluesky Jetstream | public WebSocket | e.g. `wss://jetstream2.us-east.bsky.network/subscribe` | @@ -31,7 +31,7 @@ answer for cross-SDK sharing is **interop/v1** (`protocol/spec/interop-mode.md`) encoding of the flat bound argument array. - Values are one plain MessagePack document (no ByteStorage envelope, no LZ4). - All three SDKs ship it: `cachekit-py` 0.15.0 (`@cache(interop=…)`), `@cachekit-io/cachekit` - 0.1.3 (`generateInteropKey` / `interop` wrap option), `cachekit-rs` 0.5.0 + 0.1.3 (`generateInteropKey` / `interop` wrap option), `cachekit-rs` 0.5.0+ (`interop_key()` / `#[cachekit(interop = …)]`). - Note: the spec header in `protocol/spec/interop-mode.md` still says "NOT yet implemented in any SDK" — stale; all three implementations exist and are vector-verified. Flagged upstream. @@ -117,9 +117,9 @@ config-level custom-host override alongside the credentials: - Python: env `CACHEKIT_API_URL=https://api.dev.cachekit.io` + `CACHEKIT_ALLOW_CUSTOM_HOST=true` - TS: `cachekitio({ apiUrl, allowCustomHost: true })` -- Rust: `WorkersCachekitIO::builder().api_url(...).allow_custom_host(true)` — **currently - bypassed**: `WorkersCachekitIO` panics on every wasm32 request (LAB-1079), so the hot path does a - direct `worker::Fetch` GET until the SDK fix ships (`hotpath/README.md`) +- Rust: `WorkersCachekitIO::builder().api_url(...).allow_custom_host(true)` (live since + cachekit-rs 0.7.0 fixed the wasm32 `SystemTime` panic, LAB-1079; the interim direct + `worker::Fetch` workaround was removed in LAB-1492) Round-trip verified end-to-end: `spike/roundtrip/roundtrip.py` (exists, runs against `api.dev.cachekit.io`; passed against the dev instance on 2026-07-29; also exercises `@cache.io`). @@ -143,9 +143,9 @@ Round-trip verified end-to-end: `spike/roundtrip/roundtrip.py` (exists, runs aga worker-build exactly for that reason, so the two must be bumped together; CI's exact incantation is in `.github/workflows/hotpath-qa.yml`. - On wasm32 the CachekitIO backend is `cachekit::backend::workers::WorkersCachekitIO` (CF Fetch - API); the reqwest-based `CachekitIO` does not implement `Backend` on that target. **LAB-1079**: - `WorkersCachekitIO` panics on every live wasm32 request (`SystemTime::now()` in its session - headers) — the hot path substitutes a direct `worker::Fetch` GET until the SDK fix is published. + API); the reqwest-based `CachekitIO` does not implement `Backend` on that target. **LAB-1079** + (`SystemTime::now()` panic in its session headers, affected 0.2.0–0.6.0) was fixed in + cachekit-rs 0.7.0 — the hot path's interim direct `worker::Fetch` GET was removed in LAB-1492. - Workers builds: `--no-default-features --features workers,cachekitio` (`l1`/moka and `redis`/fred are native-only; `encryption`/`macros` were dropped in `dcb6da0` — the hot path never used them, see `hotpath/Cargo.toml`). @@ -156,7 +156,7 @@ Round-trip verified end-to-end: `spike/roundtrip/roundtrip.py` (exists, runs aga `op://cachekit/ck-dev-bluesky-default`; `spike/roundtrip/roundtrip.py` round-trip verified against `api.dev.cachekit.io` (2026-07-29), closing AC-1. 2. ~~**Oracle account**~~ — resolved: Render account exists (ray, 2026-07-24); Oracle dropped. -3. ~~**cachekit-rs crates.io publish**~~ — resolved by LAB-742: crates.io now carries up to - `0.8.0`; hotpath builds against `0.5.0`. +3. ~~**cachekit-rs crates.io publish**~~ — resolved by LAB-742: cachekit-rs publishes to + crates.io (latest `0.7.0`); hotpath builds against `0.7.0`. 4. **protocol/spec/interop-mode.md status header** — says "NOT yet implemented in any SDK"; all three SDKs ship it. One-line doc fix for the protocol repo owners. diff --git a/hotpath/Cargo.lock b/hotpath/Cargo.lock index 7af8851..76638fa 100644 --- a/hotpath/Cargo.lock +++ b/hotpath/Cargo.lock @@ -125,9 +125,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cachekit-core" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee6235f73aefb0dc66b9cd0d333b9da928c8192e4234357a739fe756c2f8f23" +checksum = "6aba1513135a7b92a124ad6983f7e80e5f5c78c4f9c74384079efa3fbf491eab" dependencies = [ "byteorder", "bytes", @@ -142,9 +142,9 @@ dependencies = [ [[package]] name = "cachekit-rs" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602e017ad7eb735ba12665f217ef7c887b697fb6a6d392486c8e84e2c4865524" +checksum = "7cf3f868e64c4120e4239b7855ae90c822ea39310e82f7fc54c2e8dbdd169436" dependencies = [ "async-trait", "blake2", @@ -158,6 +158,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.19", + "tokio", "url", "urlencoding", "uuid", @@ -1173,7 +1174,6 @@ dependencies = [ "hex", "serde", "serde_json", - "urlencoding", "wasm-bindgen", "worker", ] diff --git a/hotpath/Cargo.toml b/hotpath/Cargo.toml index c2bc16f..0d5fb8e 100644 --- a/hotpath/Cargo.toml +++ b/hotpath/Cargo.toml @@ -7,8 +7,8 @@ publish = false # Stage-2 hot-path Worker (LAB-746). Build-chain pins are locked in # docs/architecture.md — worker-build@^0.1, wasm-bindgen-cli 0.2.126. # cachekit-rs comes from crates.io since 0.5.0 (LAB-742 resolved the -# failed 0.4.0 publish; 0.5.0 keeps worker pinned at 0.4, so the chain -# pins are unchanged). +# failed 0.4.0 publish); 0.7.0 (LAB-1079 wasm fix) keeps worker pinned +# at 0.4, so the chain pins are unchanged. [lib] # rlib so `cargo test` can link the compute module natively; cdylib for wasm32. @@ -17,10 +17,11 @@ crate-type = ["cdylib", "rlib"] [dependencies] # Interop key derivation + strict interop/v1 (de)serialization. No features: # the interop module is unconditional, and native builds don't need backends. -cachekit-rs = { version = "0.5.0", default-features = false } +cachekit-rs = { version = "0.7.0", default-features = false } # xxHash3-64 integrity primitive — same function that backs StorageEnvelope -# checksums, so wire values are identical. -cachekit-core = { version = "0.3", default-features = false, features = ["checksum"] } +# checksums, so wire values are identical. 0.4 matches cachekit-rs 0.7.0's +# pin so only one copy lands in the wasm binary. +cachekit-core = { version = "0.4", default-features = false, features = ["checksum"] } serde = { version = "1", features = ["derive"] } hex = "0.4" @@ -28,13 +29,9 @@ hex = "0.4" worker = "0.4" serde_json = "1" base64 = "0.22" -# Same key encoding WorkersCachekitIO uses; already in the tree via cachekit-rs. -# Direct dep for the LAB-1079 workaround fetch in lib.rs. -urlencoding = "2" # Pin the bindgen ABI to the pre-installed CLI (docs/architecture.md build pins). wasm-bindgen = "=0.2.126" -# `workers, cachekitio` are staged for the LAB-1079 swap-back to -# WorkersCachekitIO — nothing uses them today (the workaround fetches via the -# `worker` crate directly; only cachekit::interop is consumed, and that module -# is unconditional). `encryption`/`macros` were never used: dropped. -cachekit-rs = { version = "0.5.0", default-features = false, features = ["workers", "cachekitio"] } +# `workers, cachekitio` give the cache route WorkersCachekitIO (LAB-1492 +# swap-back; the wasm32 SystemTime panic was fixed in 0.7.0, LAB-1079). +# `encryption`/`macros` were never used: dropped. +cachekit-rs = { version = "0.7.0", default-features = false, features = ["workers", "cachekitio"] } diff --git a/hotpath/README.md b/hotpath/README.md index dca88ae..b2081a0 100644 --- a/hotpath/README.md +++ b/hotpath/README.md @@ -1,6 +1,6 @@ # Skyline hot-path Worker (`hotpath/`) -The Rust-WASM leg of the Skyline edge (Stage 2, LAB-746): `cachekit-rs` 0.5.0 (crates.io) +The Rust-WASM leg of the Skyline edge (Stage 2, LAB-746): `cachekit-rs` 0.7.0 (crates.io) compiled to `wasm32-unknown-unknown`, deployed as its own Cloudflare Worker. Stage 3 binds it into the TS serving path (service binding); until then it runs standalone. @@ -13,7 +13,7 @@ Dev deployment: **https://skyline-hotpath.raywalker.workers.dev** | interop/v1 key derivation | `GET /v1/key/:operation/:window` | The five locked operations × `5m`/`1h`/`24h` (contract: [`docs/architecture.md`](../docs/architecture.md)). Returns the key + locked TTL. Off-contract input → 400. | | Payload integrity | `POST /v1/verify[?expected=<16-hex>]` | Body = raw cached payload. Returns xxHash3-64 (big-endian hex, the `StorageEnvelope` convention) + strict interop/v1 validity (single MessagePack document, no trailing bytes, CK frames flagged with a diagnostic). | | Window-slice aggregation | `POST /v1/merge` | JSON `{"slices": ["", …], "top": 50}` → merged top-N counts (count desc, key asc) + the canonical interop/v1 MessagePack of the result, ready to write back byte-identically. | -| Cache read + verify | `GET /v1/cache/:operation/:window` | Derives the key, fetches the live backend, checksums + strict-decodes the payload. 503 only if the `CACHEKIT_API_KEY` secret is missing (set since Stage 3). The fetch is a direct `worker::Fetch` GET — **LAB-1079 workaround**: `WorkersCachekitIO` (cachekit-rs ≤ 0.8.0) panics on wasm32 (`SystemTime::now()` in its session headers); swap back once the SDK fix ships. Key derivation, interop decode and checksum stay on cachekit-rs / cachekit-core. | +| Cache read + verify | `GET /v1/cache/:operation/:window` | Derives the key, fetches the live backend via `WorkersCachekitIO` (the LAB-1079 wasm32 `SystemTime` panic was fixed in cachekit-rs 0.7.0; the direct `worker::Fetch` workaround is gone — LAB-1492), checksums + strict-decodes the payload. Failure statuses: 503 if the `CACHEKIT_API_KEY` secret is missing (set since Stage 3), 500 if the backend config is invalid, 502 if the backend request fails. | | Service info | `GET /` | Contract summary + endpoint list; doubles as a health check. | Example — the byte-locked spike vector, derived live on the edge: @@ -36,8 +36,8 @@ Build-chain pins are locked in [`docs/architecture.md`](../docs/architecture.md# `worker-build@^0.1`, `wasm-bindgen-cli` **0.2.126** seeded into worker-build's cache — PATH is ignored, see the architecture doc (Cargo.toml pins the `wasm-bindgen` crate to `=0.2.126` and the committed `Cargo.lock` holds the full graph, so -CLI and crate ABI can never drift). `cachekit-rs` comes from **crates.io 0.5.0** — the -spec's git-tag workaround retired when LAB-742's publish landed; 0.5.0 keeps `worker` +CLI and crate ABI can never drift). `cachekit-rs` comes from **crates.io 0.7.0** — the +spec's git-tag workaround retired when LAB-742's publish landed; 0.7.0 keeps `worker` pinned at 0.4, so the chain pins are unchanged. ```console diff --git a/hotpath/src/lib.rs b/hotpath/src/lib.rs index 5550c13..4b9c93b 100644 --- a/hotpath/src/lib.rs +++ b/hotpath/src/lib.rs @@ -9,14 +9,17 @@ //! - `GET /v1/key/:operation/:window` — interop/v1 key derivation //! - `POST /v1/verify[?expected=<16-hex>]` — xxHash3-64 + interop validity of the raw body //! - `POST /v1/merge` — merge count-map window slices (JSON: `{slices: [base64…], top}`) -//! - `GET /v1/cache/:operation/:window` — derive key, fetch via CachekitIO, -//! verify + decode (503 until the Stage-3 `CACHEKIT_API_KEY` secret exists) +//! - `GET /v1/cache/:operation/:window` — derive key, fetch via +//! `WorkersCachekitIO`, verify + decode (503 until the Stage-3 +//! `CACHEKIT_API_KEY` secret exists) pub mod compute; #[cfg(target_arch = "wasm32")] mod edge { use base64::Engine as _; + use cachekit::backend::workers::WorkersCachekitIO; + use cachekit::backend::Backend as _; use serde::Deserialize; use serde_json::json; use worker::*; @@ -43,7 +46,7 @@ mod edge { Response::from_json(&json!({ "component": "skyline-hotpath", "version": env!("CARGO_PKG_VERSION"), - "sdk": "cachekit-rs 0.5.0 (crates.io, wasm32-unknown-unknown)", + "sdk": "cachekit-rs 0.7.0 (crates.io, wasm32-unknown-unknown)", "namespace": compute::NAMESPACE, "operations": compute::OPERATIONS, "windows": { "5m": 60, "1h": 300, "24h": 900 }, @@ -138,44 +141,6 @@ mod edge { } } - /// Raw CachekitIO GET over `worker::Fetch`. - /// - /// LAB-1079 workaround: `WorkersCachekitIO` (cachekit-rs ≤ 0.8.0) panics on - /// every request — its `session_headers()` calls `SystemTime::now()`, which - /// is unimplemented on wasm32-unknown-unknown. Until the SDK fix ships, - /// the hot path does the one HTTP verb it needs directly; key derivation, - /// strict interop/v1 decode and the xxHash3 checksum stay on cachekit-rs / - /// cachekit-core. Swap back to `WorkersCachekitIO::builder()` (with - /// `.api_url(...).allow_custom_host(true)` for the dev instance) once - /// LAB-1079 is fixed and published. - async fn backend_get(api_url: &str, api_key: &str, key: &str) -> std::result::Result>, String> { - let url = format!("{}/v1/cache/{}", api_url.trim_end_matches('/'), urlencoding::encode(key)); - let mut headers = Headers::new(); - headers - .set("Authorization", &format!("Bearer {api_key}")) - .map_err(|e| format!("failed to set auth header: {e}"))?; - // SDK-class keys require an L1 status on every /v1/cache request. - headers - .set("X-CacheKit-L1-Status", "disabled") - .map_err(|e| format!("failed to set header: {e}"))?; - - let mut init = RequestInit::new(); - init.with_method(Method::Get).with_headers(headers); - let request = Request::new_with_init(&url, &init).map_err(|e| format!("failed to build request: {e}"))?; - let mut resp = Fetch::Request(request) - .send() - .await - .map_err(|e| format!("fetch failed: {e}"))?; - - match resp.status_code() { - 200 => Ok(Some(resp.bytes().await.map_err(|e| format!("failed to read body: {e}"))?)), - 404 => Ok(None), - // Never echo the response body: it is not ours and error bodies - // can carry request context. Status code only. - status => Err(format!("backend returned HTTP {status}")), - } - } - async fn cache_handler(ctx: &RouteContext<()>) -> Result { let (operation, window) = path_params(ctx)?; let key = match compute::derive_key(operation, window) { @@ -192,13 +157,37 @@ mod edge { .var("CACHEKIT_API_URL") .map(|v| v.to_string()) .unwrap_or_else(|_| "https://api.cachekit.io".to_string()); - let bytes = match backend_get(&api_url, &api_key.to_string(), &key).await { + // allow_custom_host: CACHEKIT_API_URL is trusted operator config (the + // dev instance is outside the SDK's SSRF allowlist); HTTPS + private-IP + // checks still apply. + let backend = match WorkersCachekitIO::builder() + .api_key(api_key.to_string()) + .api_url(api_url) + .allow_custom_host(true) + .build() + { + Ok(backend) => backend, + // Full error to Worker logs; the client gets a fixed message — a + // config failure is an operator problem, and the SDK's wording + // (which could name the endpoint) is not part of our contract. + Err(e) => { + console_error!("WorkersCachekitIO build failed: {e}"); + return json_error(500, "backend configuration error"); + } + }; + let bytes = match backend.get(&key).await { Ok(Some(bytes)) => bytes, Ok(None) => { return Response::from_json(&json!({ "key": key, "found": false })) .map(|r| r.with_status(404)); } - Err(e) => return json_error(502, &format!("CachekitIO error: {e}")), + // Full error to Worker logs; the client gets the error class only — + // the SDK message can embed backend response body content, which is + // not ours to relay. + Err(e) => { + console_error!("CachekitIO get failed: {e}"); + return json_error(502, &format!("CachekitIO {} error", e.kind)); + } }; let report = compute::verify_payload(&bytes, None); let value = cachekit::interop::deserialize::(&bytes).ok(); diff --git a/hotpath/wrangler.toml b/hotpath/wrangler.toml index 4bea2b6..4335bd2 100644 --- a/hotpath/wrangler.toml +++ b/hotpath/wrangler.toml @@ -8,11 +8,10 @@ compatibility_date = "2026-07-01" command = "worker-build --release" [vars] -# The dev.cachekit instance (docs/architecture.md#credentials). The LAB-1079 -# raw-fetch workaround in lib.rs bypasses the SDK's SSRF allowlist entirely — -# this value is trusted operator config, a deploy-time decision. Restore -# `.api_url(...).allow_custom_host(true)` when swapping back to -# WorkersCachekitIO. +# The dev.cachekit instance (docs/architecture.md#credentials). Outside the +# SDK's SSRF host allowlist, so lib.rs builds WorkersCachekitIO with +# `.allow_custom_host(true)` — this value is trusted operator config, a +# deploy-time decision. CACHEKIT_API_URL = "https://api.dev.cachekit.io" # Secrets (set via `wrangler secret put`, never in this file):