flaron_sdk/lib.rs
1//! # flaron-sdk
2//!
3//! Rust SDK for building **flares** - Wasm functions that run on the
4//! [Flaron][flaron] CDN edge. A flare receives an HTTP request (or WebSocket
5//! event) at the nearest edge, runs your Rust code in a sandboxed Wasm
6//! runtime, and returns a response with single-digit-millisecond latency.
7//!
8//! [flaron]: https://flaron.dev
9//!
10//! ## Quick start
11//!
12//! ```toml
13//! # Cargo.toml
14//! [package]
15//! name = "my-flare"
16//! version = "0.1.0"
17//! edition = "2021"
18//!
19//! [lib]
20//! crate-type = ["cdylib"]
21//!
22//! [dependencies]
23//! flaron-sdk = "0.1"
24//! ```
25//!
26//! ```ignore
27//! // src/lib.rs
28//! use flaron_sdk::{request, response, FlareAction};
29//!
30//! flaron_sdk::handle_request!(my_flare);
31//!
32//! fn my_flare() -> FlareAction {
33//! let body = format!("hello from {} {}", request::method(), request::url());
34//! response::set_status(200);
35//! response::set_header("content-type", "text/plain");
36//! response::set_body(body.as_bytes());
37//! FlareAction::Respond
38//! }
39//! ```
40//!
41//! Build with:
42//!
43//! ```sh
44//! cargo build --release --target wasm32-unknown-unknown
45//! ```
46//!
47//! Then deploy `target/wasm32-unknown-unknown/release/my_flare.wasm` to
48//! Flaron via the dashboard or `flaronctl`.
49//!
50//! ## What's in the SDK
51//!
52//! | Module | What it does |
53//! |-----------------------|-----------------------------------------------------------|
54//! | [`request`] | Read inbound request (method, URL, headers, body) |
55//! | [`response`] | Write outbound response (status, headers, body) |
56//! | [`beam`] | Outbound HTTP from the edge ([`beam::fetch`]) |
57//! | [`spark`] | Per-site KV with TTL, persisted to disk on the edge |
58//! | [`plasma`] | Cross-edge CRDT KV - counters, presence, leaderboards |
59//! | [`secrets`] | Read domain-scoped secrets allowlisted for this flare |
60//! | [`crypto`] | Hash, HMAC, AES-GCM encrypt/decrypt, JWT signing, RNG |
61//! | [`encoding`] | Base64, hex, URL encode/decode helpers |
62//! | [`id`] | UUID v4/v7, ULID, KSUID, Nanoid, Snowflake generators |
63//! | [`time`] | Timestamps in unix / ms / ns / RFC3339 / HTTP / ISO8601 |
64//! | [`logging`] | Structured logs surfaced via the edge node's slog stream |
65//! | [`ws`] | WebSocket: send, close, read events from open/message/close |
66//!
67//! ## Memory model
68//!
69//! Each flare invocation gets a fresh 256 KiB bump arena that the host writes
70//! into via the guest's exported `alloc` function. The SDK resets the arena
71//! at the top of every invocation so memory is reclaimed automatically - you
72//! never need to free anything yourself.
73//!
74//! The recommended entrypoint is the [`handle_request!`] macro for HTTP
75//! flares and [`ws_handlers!`] for WebSocket flares - both wire up the
76//! `alloc` export, reset the arena on every invocation, and call your
77//! handler. Use [`export_alloc!`] + [`reset_arena`] manually only when you
78//! need to define the host exports yourself.
79
80mod ffi;
81pub(crate) mod mem;
82
83pub mod beam;
84pub mod crypto;
85pub mod encoding;
86pub mod id;
87pub mod logging;
88pub mod plasma;
89pub mod request;
90pub mod response;
91pub mod secrets;
92pub mod spark;
93pub mod time;
94pub mod ws;
95
96pub use beam::{BeamError, FetchOptions, FetchResponse};
97pub use crypto::{CryptoError, RandomBytesError};
98pub use mem::{guest_alloc, reset_arena};
99pub use plasma::PlasmaError;
100pub use spark::{SparkEntry, SparkError, SparkPullError};
101pub use ws::WsSendError;
102
103/// Action returned by an `handle_request()` export to tell the host how to
104/// proceed once the flare's body has run.
105///
106/// The flaron host runtime decodes this from the high 32 bits of the i64
107/// return value (`(action << 32)` - produced by [`FlareAction::to_i64`]).
108#[repr(u32)]
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum FlareAction {
111 /// Send the response the flare just constructed (status / headers /
112 /// body set via [`response`]). This is the typical case.
113 Respond = 1,
114
115 /// Forward to origin and let the flare transform the upstream response
116 /// before it is sent back to the client.
117 Transform = 2,
118
119 /// Skip the flare entirely on this request - pass through to origin
120 /// untouched. Useful for conditional bypass logic.
121 PassThrough = 3,
122}
123
124impl FlareAction {
125 /// Encode this action as the i64 return value of `handle_request`.
126 ///
127 /// The host expects the action enum in the upper 32 bits - anything in
128 /// the lower 32 bits is reserved for future use.
129 pub fn to_i64(self) -> i64 {
130 ((self as u64) << 32) as i64
131 }
132}
133
134/// Export the guest `alloc` function the flaron host runtime requires.
135///
136/// Call this once at the crate root of your flare:
137///
138/// ```ignore
139/// flaron_sdk::export_alloc!();
140/// ```
141///
142/// This expands to a `#[no_mangle]` `extern "C" fn alloc(size: i32) -> i32`
143/// that delegates to [`guest_alloc`]. Without it, every host function that
144/// returns data to the guest will fail.
145///
146/// Most flares should use [`handle_request!`] or [`ws_handlers!`] instead -
147/// those macros include the `alloc` export for you.
148#[macro_export]
149macro_rules! export_alloc {
150 () => {
151 #[no_mangle]
152 pub extern "C" fn alloc(size: i32) -> i32 {
153 $crate::guest_alloc(size)
154 }
155 };
156}
157
158/// Wire up an HTTP flare entrypoint with a single line.
159///
160/// Pass the name of a function that takes no arguments and returns a
161/// [`FlareAction`]. The macro expands to:
162///
163/// - the `alloc` export the host runtime requires (via [`export_alloc!`]),
164/// - a `#[no_mangle] extern "C" fn handle_request() -> i64` that resets the
165/// bump arena, calls your handler, and encodes the action for the host.
166///
167/// Read inbound request data from the [`request`] module and write your
168/// response with [`response`]. Returning the action - rather than calling a
169/// host function - lets the host decide whether to ship the response,
170/// transform an upstream response, or pass through to origin.
171///
172/// ```ignore
173/// use flaron_sdk::{request, response, FlareAction};
174///
175/// flaron_sdk::handle_request!(my_flare);
176///
177/// fn my_flare() -> FlareAction {
178/// let body = format!("hello from {} {}", request::method(), request::url());
179/// response::set_status(200);
180/// response::set_header("content-type", "text/plain");
181/// response::set_body_str(&body);
182/// FlareAction::Respond
183/// }
184/// ```
185///
186/// Use [`ws_handlers!`] for WebSocket flares instead - the two macros are
187/// mutually exclusive within one crate because they both define `alloc`.
188#[macro_export]
189macro_rules! handle_request {
190 ($handler:ident) => {
191 $crate::export_alloc!();
192
193 #[no_mangle]
194 pub extern "C" fn handle_request() -> i64 {
195 $crate::reset_arena();
196 let action: $crate::FlareAction = $handler();
197 action.to_i64()
198 }
199 };
200}
201
202/// Wire up a WebSocket flare entrypoint with a single line.
203///
204/// Pass three function names in `open, message, close` order. Each function
205/// takes no arguments and returns nothing. The macro expands to:
206///
207/// - the `alloc` export the host runtime requires (via [`export_alloc!`]),
208/// - three `#[no_mangle] extern "C"` exports - `ws_open`, `ws_message`,
209/// `ws_close` - that each reset the bump arena and dispatch to your
210/// handler.
211///
212/// Read connection state and inbound frame data from the [`ws`] module
213/// (`ws::conn_id`, `ws::event_text`, `ws::close_code`, etc.) and send
214/// outbound frames with `ws::send_text` / `ws::send_binary`.
215///
216/// ```ignore
217/// use flaron_sdk::{logging, ws};
218///
219/// flaron_sdk::ws_handlers!(on_open, on_message, on_close);
220///
221/// fn on_open() {
222/// let conn = ws::conn_id();
223/// logging::info(&format!("ws open conn={}", conn));
224/// let _ = ws::send_text(&format!("welcome {}", conn));
225/// }
226///
227/// fn on_message() {
228/// let payload = ws::event_text();
229/// let _ = ws::send_text(&format!("echo: {}", payload));
230/// }
231///
232/// fn on_close() {
233/// let code = ws::close_code();
234/// logging::info(&format!("ws close code={}", code));
235/// }
236/// ```
237///
238/// Use [`handle_request!`] for HTTP flares instead - the two macros are
239/// mutually exclusive within one crate because they both define `alloc`.
240#[macro_export]
241macro_rules! ws_handlers {
242 ($open:ident, $message:ident, $close:ident) => {
243 $crate::export_alloc!();
244
245 #[no_mangle]
246 pub extern "C" fn ws_open() -> i64 {
247 $crate::reset_arena();
248 $open();
249 0
250 }
251
252 #[no_mangle]
253 pub extern "C" fn ws_message() -> i64 {
254 $crate::reset_arena();
255 $message();
256 0
257 }
258
259 #[no_mangle]
260 pub extern "C" fn ws_close() -> i64 {
261 $crate::reset_arena();
262 $close();
263 0
264 }
265 };
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn flare_action_respond_in_high_bits() {
274 let i = FlareAction::Respond.to_i64();
275 assert_eq!(i, 1i64 << 32);
276 assert_eq!((i as u64) >> 32, 1);
277 assert_eq!((i as u64) & 0xFFFF_FFFF, 0);
278 }
279
280 #[test]
281 fn flare_action_transform_in_high_bits() {
282 let i = FlareAction::Transform.to_i64();
283 assert_eq!(i, 2i64 << 32);
284 }
285
286 #[test]
287 fn flare_action_pass_through_in_high_bits() {
288 let i = FlareAction::PassThrough.to_i64();
289 assert_eq!(i, 3i64 << 32);
290 }
291
292 #[test]
293 fn flare_action_values_distinct() {
294 assert_ne!(
295 FlareAction::Respond.to_i64(),
296 FlareAction::Transform.to_i64()
297 );
298 assert_ne!(
299 FlareAction::Transform.to_i64(),
300 FlareAction::PassThrough.to_i64()
301 );
302 }
303
304 #[test]
305 fn flare_action_repr_matches_enum_value() {
306 assert_eq!(FlareAction::Respond as u32, 1);
307 assert_eq!(FlareAction::Transform as u32, 2);
308 assert_eq!(FlareAction::PassThrough as u32, 3);
309 }
310}