Skip to main content

cratefield_runtime_cloudflare/ports/
actor.rs

1//! The `Actors` port on Cloudflare: each actor is a Durable Object
2//! (issue #583).
3//!
4//! **The Durable Object class lives in the venture, not here.** `#[durable_object]`
5//! is a `wasm_bindgen` export and a library crate cannot export one on a
6//! venture's behalf — the same split as [`RoomDriver`](super::realtime::RoomDriver).
7//! The venture writes a two-method class and forwards it to [`ActorDriver`],
8//! which owns the protocol, the verification and the calls into the module's
9//! [`ActorHandler`](cratefield_core::ActorHandler). One object holds one actor:
10//! the Worker addresses it by `<kind>:<key>`, so two calls for the same key
11//! land on the same object and are serialized by the runtime.
12//!
13//! ```ignore
14//! #[durable_object]
15//! pub struct Actors { driver: ActorDriver }
16//!
17//! impl DurableObject for Actors {
18//!     fn new(state: State, env: Env) -> Self {
19//!         Self { driver: ActorDriver::new(state, &env, actor_handlers()) }
20//!     }
21//!     async fn fetch(&self, req: Request) -> Result<Response> { self.driver.fetch(req).await }
22//!     async fn alarm(&self) -> Result<Response> { self.driver.alarm().await }
23//! }
24//! ```
25//!
26//! `wrangler.toml` binds the class and declares it in a migration — a new
27//! class the runtime has not migrated is not created:
28//!
29//! ```toml
30//! [[durable_objects.bindings]]
31//! name = "ACTORS"
32//! class_name = "Actors"
33//!
34//! [[migrations]]
35//! tag = "v2"
36//! new_sqlite_classes = ["Actors"]
37//! ```
38//!
39//! # The protocol
40//!
41//! The Worker names the object `<kind>:<key>` and POSTs one binary frame:
42//!
43//! ```text
44//! kind length (u8) | kind | key length (u16, big endian) | key | message
45//! ```
46//!
47//! It signs the frame with `HMAC-SHA256(k, frame)`, where `k` is
48//! domain-separated from the signer's tokens:
49//! `k = HMAC-SHA256(HARNESS_SECRET, "cratefield actor protocol v1")`, and sends
50//! `base64url-nopad` of the HMAC in the `x-cratefield-actor-signature` header.
51//! The object recomputes it and compares in constant time, accepting the
52//! current secret and `HARNESS_SECRET_PREVIOUS` when configured. Anything
53//! unsigned or wrongly signed is refused **403** before the frame is parsed or
54//! storage is touched. A body over the largest frame the protocol can carry is
55//! refused **413** before it is buffered.
56//!
57//! Statuses: 200 with the reply bytes; 403 unsigned or wrongly signed; 400 a
58//! malformed frame, an invalid kind/key, or a frame that names a different
59//! actor; 404 no handler for the kind (the client maps it to
60//! [`ActorError::NotConfigured`]); 413 [`ActorError::TooLarge`]; 422 with the
61//! text [`ActorError::Handler`]; 500 with the text [`ActorError::Operation`].
62//! The client maps them back; a transport failure is an `Operation`.
63
64use std::collections::BTreeMap;
65use std::panic::AssertUnwindSafe;
66
67use async_trait::async_trait;
68use base64::Engine;
69use base64::engine::general_purpose::URL_SAFE_NO_PAD;
70use cratefield_core::{
71    ActorError, ActorHandlers, ActorStore, ActorWrites, Actors, HarnessConfig, MAX_ACTOR_KEY_BYTES,
72    MAX_ACTOR_KIND_BYTES, MAX_ACTOR_MESSAGE_BYTES, check_actor_call, run_actor_alarm,
73    run_actor_message, validate_actor_key, validate_actor_kind,
74};
75use futures_util::lock::Mutex;
76use hmac::{Hmac, KeyInit, Mac};
77use serde::de::DeserializeOwned;
78use sha2::Sha256;
79use subtle::ConstantTimeEq;
80use time::OffsetDateTime;
81use worker::d1::serde_wasm_bindgen;
82use worker::js_sys::futures::{JsFuture, future_to_promise};
83use worker::js_sys::{Date, Function, Promise, Reflect, Uint8Array};
84use worker::send::{IntoSendFuture, SendWrapper};
85use worker::wasm_bindgen::JsCast;
86use worker::wasm_bindgen::JsValue;
87use worker::wasm_bindgen::closure::Closure;
88use worker::worker_sys::{DurableObjectState, DurableObjectStorage, DurableObjectTransaction};
89use worker::{
90    Env, Headers, ListOptions, Method, ObjectNamespace, Request, RequestInit, Response,
91    Result as WorkerResult, State,
92};
93
94use super::WorkersClock;
95
96type HmacSha256 = Hmac<Sha256>;
97
98/// The header carrying the frame's `base64url-nopad` HMAC.
99const SIGNATURE_HEADER: &str = "x-cratefield-actor-signature";
100
101/// The domain-separation label the protocol key is derived with, so it can
102/// never be the signer's token key even if both are `HARNESS_SECRET`.
103const KEY_LABEL: &[u8] = b"cratefield actor protocol v1";
104
105/// The reserved key the actor's kind is persisted under, so an alarm — which
106/// arrives with no request and therefore no frame — can still name its actor.
107const META_KIND_KEY: &str = "\u{0}kind";
108
109/// The reserved key the actor's key is persisted under.
110const META_KEY_KEY: &str = "\u{0}key";
111
112/// The namespace a handler's storage keys are stored under, so they cannot
113/// collide with the reserved metadata keys above. `u:` is a prefix no reserved
114/// key uses, and the reserved keys use a NUL no handler key realistically can.
115const USER_PREFIX: &str = "u:";
116
117/// The storage key one of a handler's keys lives at.
118fn storage_key(key: &str) -> String {
119    format!("{USER_PREFIX}{key}")
120}
121
122/// The largest frame the protocol can carry: the u8 kind length, a maximum
123/// kind, the u16 key length, a maximum key and a maximum message — the frame
124/// `encode_frame` produces at the encoder's bounds. The object refuses a body
125/// larger than this before it buffers one, and again if the read body lies
126/// about its size.
127const MAX_FRAME_BYTES: usize =
128    1 + MAX_ACTOR_KIND_BYTES + 2 + MAX_ACTOR_KEY_BYTES + MAX_ACTOR_MESSAGE_BYTES;
129
130/// Whether a frame's `(kind, key)` is this object's identity.
131///
132/// The Worker addresses the object with `id_from_name("<kind>:<key>")`, so a
133/// frame routed here for a *different* name — a signed frame for another
134/// actor, or a misroute — must not overwrite this object's state. Two halves
135/// are checked:
136///
137/// * `name` is `state.id().name()`, the string the object was named with, or
138///   `None` when workerd does not expose it (it is not available in the
139///   constructor, and a `new_unique_id` object has no name);
140/// * `stored` is the `(kind, key)` a previous commit persisted under the
141///   reserved metadata keys, or `None` for an object that has never
142///   committed.
143///
144/// Either half that is present and disagrees refuses; a half that is absent
145/// is not a mismatch, because the other still applies.
146fn frame_matches_identity(
147    name: Option<&str>,
148    stored: Option<(&str, &str)>,
149    kind: &str,
150    key: &str,
151) -> bool {
152    let named = name.is_none_or(|name| name == format!("{kind}:{key}"));
153    let persisted =
154        stored.is_none_or(|(stored_kind, stored_key)| stored_kind == kind && stored_key == key);
155    named && persisted
156}
157
158/// The protocol key derived from `secret` — `HMAC-SHA256(secret, label)`.
159fn protocol_key(secret: &[u8]) -> [u8; 32] {
160    hmac_sha256(secret, KEY_LABEL)
161}
162
163/// `HMAC-SHA256(key, data)`. Any key length is accepted, so this cannot fail.
164fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
165    let mut mac =
166        <HmacSha256 as KeyInit>::new_from_slice(key).expect("HMAC accepts any key length");
167    mac.update(data);
168    let mut out = [0u8; 32];
169    out.copy_from_slice(&mac.finalize().into_bytes());
170    out
171}
172
173/// Encodes one request frame: kind length, kind, key length, key, message.
174///
175/// # Errors
176///
177/// [`ActorError::TooLarge`] when the kind or key cannot fit its length field —
178/// it cannot happen for a validated kind/key, but the wire format is what the
179/// decoder trusts, so the encoder is bounded too.
180fn encode_frame(kind: &str, key: &str, message: &[u8]) -> Result<Vec<u8>, ActorError> {
181    let kind_len = u8::try_from(kind.len()).map_err(|_| {
182        ActorError::TooLarge(format!(
183            "actor kind of {} bytes overflows the frame",
184            kind.len()
185        ))
186    })?;
187    let key_len = u16::try_from(key.len()).map_err(|_| {
188        ActorError::TooLarge(format!(
189            "actor key of {} bytes overflows the frame",
190            key.len()
191        ))
192    })?;
193    let mut frame = Vec::with_capacity(1 + kind.len() + 2 + key.len() + message.len());
194    frame.push(kind_len);
195    frame.extend_from_slice(kind.as_bytes());
196    frame.extend_from_slice(&key_len.to_be_bytes());
197    frame.extend_from_slice(key.as_bytes());
198    frame.extend_from_slice(message);
199    Ok(frame)
200}
201
202/// Decodes one request frame. The whole frame is consumed: a trailing byte
203/// that is not part of the message is impossible, because the message is
204/// whatever is left.
205///
206/// # Errors
207///
208/// [`ActorError::Operation`] when the frame is shorter than its length fields
209/// claim.
210fn decode_frame(frame: &[u8]) -> Result<(String, String, Vec<u8>), ActorError> {
211    let mut cursor = frame;
212    let (&kind_len, rest) = cursor.split_first().ok_or_else(malformed)?;
213    cursor = rest;
214    let kind_len = usize::from(kind_len);
215    let (kind, rest) = split_at(cursor, kind_len)?;
216    let (key_len, rest) = split_at(rest, 2)?;
217    let key_len = usize::from(u16::from_be_bytes([key_len[0], key_len[1]]));
218    let (key, message) = split_at(rest, key_len)?;
219    let kind = std::str::from_utf8(kind)
220        .map_err(|_| ActorError::Operation("actor frame kind is not utf-8".to_owned()))?;
221    let key = std::str::from_utf8(key)
222        .map_err(|_| ActorError::Operation("actor frame key is not utf-8".to_owned()))?;
223    Ok((kind.to_owned(), key.to_owned(), message.to_vec()))
224}
225
226fn split_at(bytes: &[u8], at: usize) -> Result<(&[u8], &[u8]), ActorError> {
227    if bytes.len() < at {
228        return Err(malformed());
229    }
230    Ok(bytes.split_at(at))
231}
232
233fn malformed() -> ActorError {
234    ActorError::Operation("actor frame is malformed".to_owned())
235}
236
237/// Signs `frame` with `key`, `base64url-nopad`.
238fn sign_frame(key: &[u8], frame: &[u8]) -> String {
239    URL_SAFE_NO_PAD.encode(hmac_sha256(key, frame))
240}
241
242/// Whether `signature` is `HMAC-SHA256(key, frame)`, compared in constant time.
243///
244/// A malformed signature is refused without a MAC, and a well-formed one is
245/// compared with `subtle` rather than `==`, so the comparison does not leak how
246/// many leading bytes matched.
247fn verify_frame(signature: &str, key: &[u8], frame: &[u8]) -> bool {
248    let Ok(bytes) = URL_SAFE_NO_PAD.decode(signature) else {
249        return false;
250    };
251    let Ok(expected) = <[u8; 32]>::try_from(bytes) else {
252        return false;
253    };
254    bool::from(hmac_sha256(key, frame).ct_eq(&expected))
255}
256
257/// The keys a deployment verifies with: the current secret first, the previous
258/// one second when `HARNESS_SECRET_PREVIOUS` is set.
259fn verification_keys(config: &HarnessConfig) -> Vec<[u8; 32]> {
260    let mut keys = vec![protocol_key(config.harness_secret.as_bytes())];
261    if let Some(previous) = &config.harness_secret_previous {
262        keys.push(protocol_key(previous.as_bytes()));
263    }
264    keys
265}
266
267/// Forwards a Durable Object's `fetch` and `alarm` entry points to the module's
268/// [`ActorHandler`](cratefield_core::ActorHandler)s.
269pub struct ActorDriver {
270    /// The object's raw state. It is held raw rather than as `worker::State`
271    /// because the transaction needs the `DurableObjectTransaction`'s alarm
272    /// methods, which `worker::Storage::transaction` hides — see `commit`.
273    /// `State::_inner` is the one way to reach the raw handle, and the
274    /// worker library offers no other way to reach them.
275    state: DurableObjectState,
276    handlers: ActorHandlers,
277    /// The keys a frame may be signed with — current, then previous.
278    keys: Vec<[u8; 32]>,
279    /// Serializes messages and alarms. A Durable Object already delivers one
280    /// request at a time, but a handler that awaits lets workerd interleave
281    /// the next event's *read* of storage with this one, so the runner's
282    /// read-modify-write is only safe behind a lock this driver holds.
283    lock: Mutex<()>,
284}
285
286impl ActorDriver {
287    /// Builds the driver from the object's `State`, its `Env` (for the signing
288    /// key) and the handlers the venture registered.
289    ///
290    /// A deployment whose `HARNESS_SECRET` is missing or too short derives no
291    /// key, and every request is then refused 403 rather than served.
292    #[must_use]
293    pub fn new(state: State, env: &Env, handlers: ActorHandlers) -> Self {
294        let keys = match HarnessConfig::from_config(&crate::config::EnvConfig(env.clone())) {
295            Ok(config) => verification_keys(&config),
296            Err(_) => Vec::new(),
297        };
298        Self {
299            state: state._inner(),
300            handlers,
301            keys,
302            lock: Mutex::new(()),
303        }
304    }
305
306    /// Handles one signed message frame.
307    ///
308    /// # Errors
309    ///
310    /// A `worker::Error` only from reading the request body; every protocol
311    /// failure is a status on the response, because the caller is a Worker that
312    /// maps statuses back to [`ActorError`]s.
313    pub async fn fetch(&self, req: Request) -> WorkerResult<Response> {
314        let mut req = req;
315        let Ok(Some(signature)) = req.headers().get(SIGNATURE_HEADER) else {
316            return Response::error("actor request is not signed", 403);
317        };
318        // Bound the body before buffering it. `Content-Length` is checked
319        // first, so a declared-oversized body is refused without reading it;
320        // the read length is checked again, because the header can lie. The
321        // signature still verifies before anything is parsed or stored.
322        if declared_content_length(&req).is_some_and(|length| length > MAX_FRAME_BYTES) {
323            return Response::error("actor frame is too large", 413);
324        }
325        let frame = req.bytes().await?;
326        if frame.len() > MAX_FRAME_BYTES {
327            return Response::error("actor frame is too large", 413);
328        }
329        if !self
330            .keys
331            .iter()
332            .any(|key| verify_frame(&signature, key, &frame))
333        {
334            return Response::error("actor request signature is invalid", 403);
335        }
336        let Ok((kind, key, message)) = decode_frame(&frame) else {
337            return Response::error("actor frame is malformed", 400);
338        };
339        if validate_actor_kind(&kind).is_err() || validate_actor_key(&key).is_err() {
340            return Response::error("actor kind or key is invalid", 400);
341        }
342        let Some(handler) = self.handlers.get(&kind) else {
343            return Response::error("no handler for this actor kind", 404);
344        };
345
346        let _serialized = self.lock.lock().await;
347        // The frame must name *this* object. The Worker routes by
348        // `id_from_name("<kind>:<key>")`, so a signed frame for another actor
349        // reaching here is refused before storage is touched: a mismatching
350        // name (where workerd exposes one) or a stored identity that disagrees
351        // with the frame.
352        let stored = read_meta(&self.state).await?;
353        let named_identity = self.state.id()?.name();
354        if !frame_matches_identity(
355            named_identity.as_deref(),
356            stored
357                .as_ref()
358                .map(|(kind, key)| (kind.as_str(), key.as_str())),
359            &kind,
360            &key,
361        ) {
362            return Response::error("actor frame names a different actor", 400);
363        }
364        let store = DurableActorStore::new(&self.state, &kind, &key);
365        match run_actor_message(
366            handler.as_ref(),
367            &store,
368            &WorkersClock,
369            &kind,
370            &key,
371            &message,
372        )
373        .await
374        {
375            Ok(reply) => Response::from_bytes(reply),
376            Err(error) => actor_error_response(&error),
377        }
378    }
379
380    /// Runs the actor's alarm. The actor's kind and key come from the reserved
381    /// keys a previous message committed, because an alarm carries no frame.
382    ///
383    /// # Errors
384    ///
385    /// When a storage read fails, and — deliberately — when the handler fails:
386    /// a failed alarm commits nothing, so workerd's retry of a throwing alarm
387    /// handler is the retry the port's contract promises. It is **not** logged
388    /// and swallowed the way [`RoomDriver`](super::realtime::RoomDriver)'s tick
389    /// is, because a room tick that is missed is a tick, and an actor alarm
390    /// that is missed is a lease that never expired.
391    pub async fn alarm(&self) -> WorkerResult<Response> {
392        let _serialized = self.lock.lock().await;
393        let Some((kind, key)) = read_meta(&self.state).await? else {
394            // No message was ever committed against this object, so it has no
395            // actor identity and nothing to run.
396            return Response::empty();
397        };
398        let Some(handler) = self.handlers.get(&kind) else {
399            return Response::empty();
400        };
401        let store = DurableActorStore::new(&self.state, &kind, &key);
402        match run_actor_alarm(handler.as_ref(), &store, &WorkersClock, &kind, &key).await {
403            Ok(()) => Response::empty(),
404            Err(error) => Err(worker::Error::RustError(error.to_string())),
405        }
406    }
407}
408
409/// The `Content-Length` a request declares, if it is present and parseable. A
410/// missing or unparseable header is `None`, and the read-length check in
411/// `fetch` still applies.
412fn declared_content_length(req: &Request) -> Option<usize> {
413    req.headers()
414        .get("content-length")
415        .ok()
416        .flatten()
417        .and_then(|value| value.parse().ok())
418}
419
420/// The actor's kind and key, persisted under the reserved keys on the first
421/// commit, or `None` for an object that has never committed.
422async fn read_meta(state: &DurableObjectState) -> WorkerResult<Option<(String, String)>> {
423    let storage = state.storage()?;
424    let kind = storage_get::<String>(&storage, META_KIND_KEY).await?;
425    let key = storage_get::<String>(&storage, META_KEY_KEY).await?;
426    Ok(match (kind, key) {
427        (Some(kind), Some(key)) => Some((kind, key)),
428        _ => None,
429    })
430}
431
432/// Reads one value through the raw storage handle.
433///
434/// The driver works in terms of `DurableObjectStorage` rather than
435/// `worker::Storage` because it needs the raw handle for the transaction's
436/// alarm methods; a `worker::State` cannot give one back. This mirrors
437/// `worker::Storage::get`, including reading an `undefined` as `None`.
438async fn storage_get<T: DeserializeOwned>(
439    storage: &DurableObjectStorage,
440    key: &str,
441) -> WorkerResult<Option<T>> {
442    let value = JsFuture::from(storage.get(key)?).into_send().await?;
443    if value.is_undefined() {
444        return Ok(None);
445    }
446    Ok(Some(serde_wasm_bindgen::from_value(value)?))
447}
448
449/// Every `(raw key, value)` under `prefix`, sorted by key, through the raw
450/// storage handle. The one place a listing is decoded, shared by the store's
451/// `list_prefix` and its `clear_all` key set.
452async fn storage_list(
453    storage: &DurableObjectStorage,
454    prefix: &str,
455) -> WorkerResult<BTreeMap<String, Vec<u8>>> {
456    let options = ListOptions::new().prefix(prefix);
457    let promise = storage.list_with_options(serde_wasm_bindgen::to_value(&options)?.into())?;
458    let listed = JsFuture::from(promise).into_send().await?;
459    serde_wasm_bindgen::from_value(listed).map_err(Into::into)
460}
461
462/// The pending alarm as milliseconds since the epoch, through the raw storage
463/// handle, or `None` when none is set.
464async fn storage_alarm(storage: &DurableObjectStorage) -> WorkerResult<Option<i64>> {
465    let value = JsFuture::from(storage.get_alarm(JsValue::NULL.into())?)
466        .into_send()
467        .await?;
468    // The alarm is milliseconds since the epoch, a whole number well inside
469    // `i64`'s range; the cast only narrows the `f64` a JS number arrives as.
470    #[allow(clippy::cast_possible_truncation)]
471    Ok(value.as_f64().map(|ms| ms as i64))
472}
473
474/// Maps a runner failure onto the status the Worker maps back to an
475/// [`ActorError`].
476fn actor_error_response(error: &ActorError) -> WorkerResult<Response> {
477    let (status, detail) = match error {
478        ActorError::TooLarge(detail) => (413, detail.clone()),
479        ActorError::Handler(detail) => (422, detail.clone()),
480        ActorError::NotConfigured => (404, "no handler for this actor kind".to_owned()),
481        ActorError::Timeout | ActorError::Operation(_) => (500, error.to_string()),
482    };
483    Response::error(detail, status)
484}
485
486fn store_error(error: &worker::Error) -> ActorError {
487    ActorError::Operation(error.to_string())
488}
489
490/// A `JsValue` failure — from a raw binding rather than a `worker::Error` —
491/// as an [`ActorError`].
492fn js_error(error: JsValue) -> ActorError {
493    store_error(&worker::Error::from(error))
494}
495
496/// The store one Durable Object instance reads and writes through. Core owns
497/// the staging and the all-or-nothing rule; this only has to make
498/// [`commit`](ActorStore::commit) atomic and keep the reserved keys out of the
499/// handler's namespace.
500struct DurableActorStore<'a> {
501    state: &'a DurableObjectState,
502    kind: String,
503    key: String,
504}
505
506impl<'a> DurableActorStore<'a> {
507    fn new(state: &'a DurableObjectState, kind: &str, key: &str) -> Self {
508        Self {
509            state,
510            kind: kind.to_owned(),
511            key: key.to_owned(),
512        }
513    }
514
515    /// The object's raw storage handle.
516    fn storage(&self) -> Result<DurableObjectStorage, ActorError> {
517        self.state.storage().map_err(js_error)
518    }
519}
520
521#[async_trait]
522impl ActorStore for DurableActorStore<'_> {
523    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, ActorError> {
524        let storage = self.storage()?;
525        storage_get::<Vec<u8>>(&storage, &storage_key(key))
526            .await
527            .map_err(|error| store_error(&error))
528    }
529
530    async fn list_prefix(&self, prefix: &str) -> Result<Vec<(String, Vec<u8>)>, ActorError> {
531        let storage = self.storage()?;
532        // The values were stored through `put`, so each is a JSON-ish array of
533        // numbers in JS; deserializing through the same serde stack is what
534        // reads them back as bytes.
535        let entries = storage_list(&storage, &storage_key(prefix))
536            .await
537            .map_err(|error| store_error(&error))?;
538        Ok(entries
539            .into_iter()
540            .filter_map(|(key, value)| {
541                key.strip_prefix(USER_PREFIX)
542                    .map(|key| (key.to_owned(), value))
543            })
544            .collect())
545    }
546
547    async fn alarm(&self) -> Result<Option<OffsetDateTime>, ActorError> {
548        let storage = self.storage()?;
549        match storage_alarm(&storage)
550            .await
551            .map_err(|error| store_error(&error))?
552        {
553            Some(ms) => Ok(Some(ms_to_alarm(ms)?)),
554            None => Ok(None),
555        }
556    }
557
558    async fn commit(&self, writes: ActorWrites) -> Result<(), ActorError> {
559        let ActorWrites {
560            clear_all,
561            entries,
562            alarm,
563        } = writes;
564        let kind = self.kind.clone();
565        let key = self.key.clone();
566        let entries: Vec<(String, Option<Vec<u8>>)> = entries.into_iter().collect();
567
568        // **`deleteAll` is refused inside a transaction** (workerd: "Cannot
569        // call deleteAll() within a transaction"), so a `clear_all` is carried
570        // out as an explicit delete of every existing key *inside* the same
571        // transaction. The key set is read first, which is safe because the
572        // driver holds the per-actor lock: no other message or alarm for this
573        // actor can write between the read and the commit.
574        let clearing: Vec<String> = if clear_all {
575            let storage = self.storage()?;
576            storage_list(&storage, USER_PREFIX)
577                .await
578                .map_err(|error| store_error(&error))?
579                .into_keys()
580                .collect()
581        } else {
582            Vec::new()
583        };
584
585        // The values **and the alarm** commit as one atomic unit, inside one
586        // `storage.transaction`: workerd applies every put, delete and alarm
587        // change in the closure or none of them. This is why the driver holds
588        // the raw `DurableObjectState`: `worker::Storage::transaction` hands a
589        // `worker::Transaction` whose inner JS object is private, and
590        // `worker-sys` 0.8.5 does not bind the transaction's `getAlarm`,
591        // `setAlarm` or `deleteAlarm`, even though the JS object has them
592        // (Cloudflare's `DurableObjectTransaction` interface). Applying the
593        // alarm *after* the value transaction — as an earlier version did —
594        // lets a value land and the alarm write fail: an `expiring` value
595        // stored with no expiry, or an alarm handler re-run against already
596        // committed values.
597        let storage = self.storage()?;
598        let closure: Box<dyn FnOnce(DurableObjectTransaction) -> Promise> =
599            Box::new(move |tx: DurableObjectTransaction| -> Promise {
600                future_to_promise(AssertUnwindSafe(async move {
601                    apply_writes(tx, clearing, entries, kind, key, alarm).await
602                }))
603            });
604        // A `Closure` is not `Send` (it holds a JS handle), and the store's
605        // `commit` is an `async_trait` method whose future must be; Workers is
606        // single-threaded, so wrapping the handle to hold it across the await
607        // is sound.
608        let closure = SendWrapper::new(Closure::once_assert_unwind_safe(closure));
609        JsFuture::from(storage.transaction(&closure).map_err(js_error)?)
610            .into_send()
611            .await
612            .map_err(js_error)?;
613        Ok(())
614    }
615}
616
617/// Applies one commit inside the object's transaction: the explicit deletes a
618/// `clear_all` needs, the staged puts and deletes, the reserved identity keys,
619/// and then the alarm. Runs as the transaction closure's future; a failure
620/// rolls the whole transaction back.
621// `Option<Option<_>>` mirrors [`ActorWrites::alarm`]'s documented tri-state —
622// `Some(Some(at))` sets, `Some(None)` cancels, `None` leaves it untouched — so
623// a named enum here would only be a second spelling of the same three cases.
624#[allow(clippy::option_option)]
625async fn apply_writes(
626    tx: DurableObjectTransaction,
627    clearing: Vec<String>,
628    entries: Vec<(String, Option<Vec<u8>>)>,
629    kind: String,
630    key: String,
631    alarm: Option<Option<OffsetDateTime>>,
632) -> Result<JsValue, JsValue> {
633    for stored in clearing {
634        JsFuture::from(tx.delete(&stored)?).await?;
635    }
636    for (entry_key, value) in entries {
637        let stored = storage_key(&entry_key);
638        match value {
639            Some(bytes) => {
640                let value = serde_wasm_bindgen::to_value(&bytes)?;
641                JsFuture::from(tx.put(&stored, value)?).await?;
642            }
643            None => {
644                JsFuture::from(tx.delete(&stored)?).await?;
645            }
646        }
647    }
648    // Written last, so they survive a `clear_all` that wiped them first: the
649    // alarm that follows still knows whose it is.
650    JsFuture::from(tx.put(META_KIND_KEY, serde_wasm_bindgen::to_value(&kind)?)?).await?;
651    JsFuture::from(tx.put(META_KEY_KEY, serde_wasm_bindgen::to_value(&key)?)?).await?;
652
653    if let Some(alarm) = alarm {
654        let this: &JsValue = tx.as_ref();
655        if let Some(at) = alarm {
656            // Epoch milliseconds: an integer that a `Date` takes as a number,
657            // and `f64` represents every such value exactly up to year 287396.
658            #[allow(clippy::cast_precision_loss)]
659            let when = Date::new(&JsValue::from_f64(alarm_epoch_ms(at) as f64));
660            let set = transaction_method(&tx, "setAlarm")?;
661            let promise: Promise = set.call2(this, &when, &JsValue::NULL)?.unchecked_into();
662            JsFuture::from(promise).await?;
663        } else {
664            let delete = transaction_method(&tx, "deleteAlarm")?;
665            let promise: Promise = delete.call1(this, &JsValue::NULL)?.unchecked_into();
666            JsFuture::from(promise).await?;
667        }
668    }
669    Ok(JsValue::UNDEFINED)
670}
671
672/// Looks up one of the transaction's JS methods `worker-sys` does not bind —
673/// `setAlarm` or `deleteAlarm`. The transaction is a JS object, so the method
674/// is read off it with `Reflect`; no `unsafe` and no new binding are needed.
675fn transaction_method(tx: &DurableObjectTransaction, name: &str) -> Result<Function, JsValue> {
676    let object: &JsValue = tx.as_ref();
677    Reflect::get(object, &JsValue::from_str(name))?.dyn_into()
678}
679
680/// A pending alarm as milliseconds since the epoch, or an error if the stored
681/// value is not a representable time.
682fn ms_to_alarm(ms: i64) -> Result<OffsetDateTime, ActorError> {
683    OffsetDateTime::from_unix_timestamp_nanos(i128::from(ms) * 1_000_000)
684        .map_err(|error| ActorError::Operation(format!("stored alarm is not a time: {error}")))
685}
686
687/// The alarm time as milliseconds since the epoch — what a Durable Object's
688/// `setAlarm` takes as a `Date`.
689fn alarm_epoch_ms(at: OffsetDateTime) -> i64 {
690    let nanos = at.unix_timestamp_nanos();
691    let ms = nanos / 1_000_000;
692    // Bounded for the wire: a time no `i64` can hold is clamped to the end it
693    // fell off, and one that far out is a caller bug, not a schedule.
694    i64::try_from(ms).unwrap_or(if ms.is_negative() { i64::MIN } else { i64::MAX })
695}
696
697/// The `Actors` host: names the object `<kind>:<key>`, signs the frame and
698/// maps the object's status back to an [`ActorError`].
699pub(crate) struct DurableActors {
700    namespace: ObjectNamespace,
701    signing_key: Vec<u8>,
702}
703
704impl DurableActors {
705    /// Builds the host from the `ACTORS` binding and the deployment's key. The
706    /// current secret signs; a rotation is picked up by the next isolate.
707    pub(crate) fn new(namespace: ObjectNamespace, config: &HarnessConfig) -> Self {
708        Self {
709            namespace,
710            signing_key: protocol_key(config.harness_secret.as_bytes()).to_vec(),
711        }
712    }
713}
714
715#[async_trait]
716impl Actors for DurableActors {
717    async fn call(&self, kind: &str, key: &str, message: &[u8]) -> Result<Vec<u8>, ActorError> {
718        check_actor_call(kind, key, message)?;
719        if self.signing_key.is_empty() {
720            return Err(ActorError::Operation(
721                "actor host has no signing key: HARNESS_SECRET is not configured".to_owned(),
722            ));
723        }
724
725        let frame = encode_frame(kind, key, message)?;
726        let signature = sign_frame(&self.signing_key, &frame);
727        let mut init = RequestInit::new();
728        init.method = Method::Post;
729        let headers = Headers::new();
730        headers
731            .set(SIGNATURE_HEADER, &signature)
732            .map_err(|error| store_error(&error))?;
733        init.headers = headers;
734        init.with_body(Some(Uint8Array::from(frame.as_slice()).into()));
735
736        // A Durable Object stub routes by the id the name was hashed to, so the
737        // URL is a stand-in origin: workerd refuses a request with no absolute
738        // URL, and nothing reads the authority.
739        let request = Request::new_with_init("https://actors.invalid/", &init)
740            .map_err(|error| store_error(&error))?;
741        let stub = self
742            .namespace
743            .id_from_name(&format!("{kind}:{key}"))
744            .map_err(|error| store_error(&error))?
745            .get_stub()
746            .map_err(|error| store_error(&error))?;
747        let mut response = stub
748            .fetch_with_request(request)
749            .into_send()
750            .await
751            .map_err(|error| store_error(&error))?;
752
753        match response.status_code() {
754            200 => response
755                .bytes()
756                .into_send()
757                .await
758                .map_err(|error| store_error(&error)),
759            404 => Err(ActorError::NotConfigured),
760            413 => Err(ActorError::TooLarge(response_text(&mut response).await)),
761            422 => Err(ActorError::Handler(response_text(&mut response).await)),
762            status => Err(ActorError::Operation(format!(
763                "actor host answered {status}: {}",
764                response_text(&mut response).await
765            ))),
766        }
767    }
768}
769
770async fn response_text(response: &mut Response) -> String {
771    response.text().into_send().await.unwrap_or_default()
772}
773
774#[cfg(test)]
775mod tests {
776    use super::*;
777
778    /// A deployment's two keys, so the tests do not re-derive by hand.
779    fn keys() -> (HarnessConfig, [u8; 32], Option<[u8; 32]>) {
780        let config = HarnessConfig {
781            harness_secret: "test-secret-0123456789abcdef-0123".to_owned(),
782            harness_secret_previous: Some("previous-secret-0123456789abcdef".to_owned()),
783            harness_secret_revoked: Vec::new(),
784            harness_venture: None,
785            admin_token: None,
786            env: cratefield_core::VentureEnv::Development,
787        };
788        let current = protocol_key(config.harness_secret.as_bytes());
789        let previous = config
790            .harness_secret_previous
791            .as_ref()
792            .map(|secret| protocol_key(secret.as_bytes()));
793        (config, current, previous)
794    }
795
796    #[test]
797    fn a_frame_round_trips() {
798        let frame = encode_frame("counter", "user-7", b"inc").expect("fits");
799        assert_eq!(
800            decode_frame(&frame).expect("decodes"),
801            ("counter".to_owned(), "user-7".to_owned(), b"inc".to_vec())
802        );
803        // The message is whatever is left, including an empty one.
804        let frame = encode_frame("counter", "k", b"").expect("fits");
805        assert_eq!(decode_frame(&frame).expect("decodes").2, Vec::<u8>::new());
806    }
807
808    #[test]
809    fn a_malformed_frame_is_refused() {
810        // Shorter than its kind length claims.
811        assert!(decode_frame(&[4, b'a']).is_err());
812        // Shorter than its key length claims.
813        assert!(decode_frame(&[1, b'a', 0, 9, b'k']).is_err());
814        // No length byte at all.
815        assert!(decode_frame(&[]).is_err());
816        // A non-UTF-8 kind.
817        assert!(decode_frame(&[1, 0xff, 0, 1, b'k']).is_err());
818    }
819
820    #[test]
821    fn a_signature_verifies_and_a_tampered_frame_does_not() {
822        let (_, current, _) = keys();
823        let frame = encode_frame("counter", "k", b"inc").expect("fits");
824        let signature = sign_frame(&current, &frame);
825        assert!(verify_frame(&signature, &current, &frame));
826
827        let mut tampered = frame.clone();
828        tampered[0] = tampered[0].wrapping_add(1);
829        assert!(!verify_frame(&signature, &current, &tampered));
830    }
831
832    #[test]
833    fn a_signature_over_a_different_kind_or_key_is_refused() {
834        let (_, current, _) = keys();
835        let signature = sign_frame(
836            &current,
837            &encode_frame("counter", "one", b"m").expect("fits"),
838        );
839        assert!(!verify_frame(
840            &signature,
841            &current,
842            &encode_frame("counter", "two", b"m").expect("fits")
843        ));
844        assert!(!verify_frame(
845            &signature,
846            &current,
847            &encode_frame("other", "one", b"m").expect("fits")
848        ));
849    }
850
851    #[test]
852    fn a_wrong_secret_is_refused_and_the_previous_one_is_accepted() {
853        let (config, current, previous) = keys();
854        let frame = encode_frame("counter", "k", b"inc").expect("fits");
855
856        let previous = previous.expect("the fixture sets one");
857        let previous_signature = sign_frame(&previous, &frame);
858        assert!(!verify_frame(&previous_signature, &current, &frame));
859        // The driver tries both, so a frame minted before a rotation verifies.
860        let accepted = verification_keys(&config)
861            .iter()
862            .any(|key| verify_frame(&previous_signature, key, &frame));
863        assert!(accepted, "the previous secret was not accepted");
864
865        let other = protocol_key(b"a secret nobody configured");
866        let signature = sign_frame(&other, &frame);
867        assert!(
868            !verification_keys(&config)
869                .iter()
870                .any(|key| verify_frame(&signature, key, &frame))
871        );
872    }
873
874    #[test]
875    fn a_malformed_signature_is_refused() {
876        let (_, current, _) = keys();
877        let frame = encode_frame("counter", "k", b"inc").expect("fits");
878        assert!(!verify_frame("not base64!", &current, &frame));
879        assert!(!verify_frame("", &current, &frame));
880        // Valid base64, wrong length.
881        assert!(!verify_frame(
882            &URL_SAFE_NO_PAD.encode(b"short"),
883            &current,
884            &frame
885        ));
886    }
887
888    #[test]
889    fn the_protocol_key_is_domain_separated_from_the_secret() {
890        // The key is not the secret (or a bare hash of it): a token the signer
891        // mints from `HARNESS_SECRET` can never verify an actor frame.
892        let (_, current, _) = keys();
893        assert_ne!(&current[..], b"test-secret-0123456789abcdef-0123");
894    }
895
896    #[test]
897    fn an_alarm_round_trips_through_epoch_milliseconds() {
898        let now = OffsetDateTime::UNIX_EPOCH;
899        assert_eq!(
900            alarm_epoch_ms(now + time::Duration::milliseconds(1500)),
901            1500
902        );
903        assert_eq!(
904            ms_to_alarm(1500).expect("a time"),
905            now + time::Duration::milliseconds(1500)
906        );
907        assert_eq!(alarm_epoch_ms(now), 0);
908    }
909
910    #[test]
911    fn a_frame_matches_its_identity() {
912        // The name half: `id_from_name("counter:k")` must only serve a frame
913        // that names `counter`/`k`.
914        assert!(frame_matches_identity(
915            Some("counter:k"),
916            None,
917            "counter",
918            "k"
919        ));
920        assert!(!frame_matches_identity(
921            Some("counter:other"),
922            None,
923            "counter",
924            "k"
925        ));
926        assert!(!frame_matches_identity(
927            Some("other:k"),
928            None,
929            "counter",
930            "k"
931        ));
932        // A name workerd does not expose is not a mismatch — the persisted
933        // half still applies.
934        assert!(frame_matches_identity(None, None, "counter", "k"));
935
936        // The persisted half: metadata a previous commit wrote must agree.
937        assert!(frame_matches_identity(
938            None,
939            Some(("counter", "k")),
940            "counter",
941            "k"
942        ));
943        assert!(!frame_matches_identity(
944            None,
945            Some(("counter", "other")),
946            "counter",
947            "k"
948        ));
949        assert!(!frame_matches_identity(
950            None,
951            Some(("other", "k")),
952            "counter",
953            "k"
954        ));
955        // Both halves present: either one disagreeing refuses.
956        assert!(!frame_matches_identity(
957            Some("counter:k"),
958            Some(("counter", "other")),
959            "counter",
960            "k"
961        ));
962    }
963}