Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| 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` |
Expand All @@ -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.
Expand Down Expand Up @@ -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`).
Expand All @@ -143,19 +143,19 @@ 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.
- Workers builds: `--no-default-features --features workers,cachekitio,encryption,macros`
(`l1`/moka and `redis`/fred are native-only).
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` are unused and dropped).

## Open items (flagged, not blocking the spec)

1. ~~**CachekitIO credentials**~~ — resolved: creds exist at
`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.
10 changes: 5 additions & 5 deletions hotpath/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 10 additions & 13 deletions hotpath/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -17,24 +17,21 @@ 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 }
Comment thread
27Bslash6 marked this conversation as resolved.
# 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"

[target.'cfg(target_arch = "wasm32")'.dependencies]
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"] }
8 changes: 4 additions & 4 deletions hotpath/README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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": ["<base64 msgpack {str:int} doc>", …], "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:
Expand All @@ -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
Expand Down
75 changes: 32 additions & 43 deletions hotpath/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -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 },
Expand Down Expand Up @@ -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<Option<Vec<u8>>, 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<Response> {
let (operation, window) = path_params(ctx)?;
let key = match compute::derive_key(operation, window) {
Expand All @@ -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::<serde_json::Value>(&bytes).ok();
Expand Down
9 changes: 4 additions & 5 deletions hotpath/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down