1use std::cell::RefCell;
36use std::collections::HashMap;
37
38use serde::de::DeserializeOwned;
39use serde::Serialize;
40use wasm_bindgen::prelude::*;
41
42use crate::context::push_cookie;
43use crate::error::{CfError, CfResultExt};
44
45#[wasm_bindgen]
50extern "C" {
51 #[wasm_bindgen(js_namespace = crypto, js_name = randomUUID)]
52 fn random_uuid() -> String;
53}
54
55#[derive(Clone)]
64pub struct SessionConfig {
65 pub(crate) backend: SessionBackend,
66 pub(crate) cookie_name: String,
67 pub(crate) max_age_secs: u64,
68}
69
70#[derive(Clone)]
71pub(crate) enum SessionBackend {
72 Kv { binding: String },
73 D1 { binding: String, table: String },
74}
75
76impl SessionConfig {
77 #[must_use]
91 pub fn kv(binding: &str) -> Self {
92 Self {
93 backend: SessionBackend::Kv {
94 binding: binding.to_owned(),
95 },
96 cookie_name: "__session".to_owned(),
97 max_age_secs: 86400,
98 }
99 }
100
101 #[must_use]
125 pub fn d1(binding: &str, table: &str) -> Self {
126 Self {
127 backend: SessionBackend::D1 {
128 binding: binding.to_owned(),
129 table: table.to_owned(),
130 },
131 cookie_name: "__session".to_owned(),
132 max_age_secs: 86400,
133 }
134 }
135
136 #[must_use]
138 pub fn cookie_name(mut self, name: &str) -> Self {
139 self.cookie_name = name.to_owned();
140 self
141 }
142
143 #[must_use]
148 pub fn max_age(mut self, secs: u64) -> Self {
149 self.max_age_secs = secs;
150 self
151 }
152}
153
154struct SessionState {
159 id: String,
160 data: HashMap<String, serde_json::Value>,
161 dirty: bool,
162 destroyed: bool,
163 is_new: bool,
164}
165
166thread_local! {
167 static SESSION_CONFIG: RefCell<Option<SessionConfig>> = const { RefCell::new(None) };
168 static SESSION_STATE: RefCell<Option<SessionState>> = const { RefCell::new(None) };
169}
170
171pub struct Session;
180
181impl Session {
182 pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, CfError> {
190 SESSION_STATE.with(|cell| {
191 let borrow = cell.borrow();
192 let state = borrow
193 .as_ref()
194 .ok_or_else(|| CfError(worker::Error::RustError("session not loaded".into())))?;
195
196 match state.data.get(key) {
197 Some(value) => {
198 let typed: T = serde_json::from_value(value.clone()).cf()?;
199 Ok(Some(typed))
200 }
201 None => Ok(None),
202 }
203 })
204 }
205
206 pub fn set<T: Serialize>(&self, key: &str, value: &T) -> Result<(), CfError> {
215 SESSION_STATE.with(|cell| {
216 let mut borrow = cell.borrow_mut();
217 let state = borrow
218 .as_mut()
219 .ok_or_else(|| CfError(worker::Error::RustError("session not loaded".into())))?;
220
221 let json_value = serde_json::to_value(value).cf()?;
222 state.data.insert(key.to_owned(), json_value);
223 state.dirty = true;
224 Ok(())
225 })
226 }
227
228 pub fn remove(&self, key: &str) {
230 SESSION_STATE.with(|cell| {
231 if let Some(ref mut state) = *cell.borrow_mut() {
232 if state.data.remove(key).is_some() {
233 state.dirty = true;
234 }
235 }
236 });
237 }
238
239 pub fn destroy(&self) {
241 SESSION_STATE.with(|cell| {
242 if let Some(ref mut state) = *cell.borrow_mut() {
243 state.destroyed = true;
244 }
245 });
246 }
247
248 pub fn is_empty(&self) -> bool {
250 SESSION_STATE.with(|cell| {
251 cell.borrow()
252 .as_ref()
253 .is_none_or(|state| state.data.is_empty())
254 })
255 }
256
257 pub fn is_new(&self) -> bool {
259 SESSION_STATE.with(|cell| {
260 cell.borrow()
261 .as_ref()
262 .is_none_or(|state| state.is_new)
263 })
264 }
265}
266
267pub async fn session() -> Result<Session, CfError> {
292 let already_loaded = SESSION_STATE.with(|cell| cell.borrow().is_some());
294 if already_loaded {
295 return Ok(Session);
296 }
297
298 let config = SESSION_CONFIG.with(|cell| cell.borrow().clone()).ok_or_else(|| {
300 CfError(worker::Error::RustError(
301 "cf::session() called but session middleware is not configured — \
302 add .session(SessionConfig::kv(\"SESSIONS\")) to your Handler"
303 .into(),
304 ))
305 })?;
306
307 let existing_id = crate::cookie::cookie(&config.cookie_name)?;
309
310 let (id, data, is_new) = match existing_id {
311 Some(ref session_id) if !session_id.is_empty() => {
312 let data = load_from_backend(&config, session_id).await?;
314 match data {
315 Some(d) => (session_id.clone(), d, false),
316 None => (random_uuid(), HashMap::new(), true),
318 }
319 }
320 _ => {
321 (random_uuid(), HashMap::new(), true)
323 }
324 };
325
326 SESSION_STATE.with(|cell| {
327 *cell.borrow_mut() = Some(SessionState {
328 id,
329 data,
330 dirty: false,
331 destroyed: false,
332 is_new,
333 });
334 });
335
336 Ok(Session)
337}
338
339pub(crate) fn set_session_config(config: SessionConfig) {
345 SESSION_CONFIG.with(|cell| *cell.borrow_mut() = Some(config));
346}
347
348pub(crate) async fn flush_session() -> Result<(), worker::Error> {
353 let state = SESSION_STATE.with(|cell| cell.borrow_mut().take());
355 let Some(state) = state else {
356 return Ok(());
357 };
358
359 let config = SESSION_CONFIG.with(|cell| cell.borrow().clone());
360 let Some(config) = config else {
361 return Ok(());
362 };
363
364 if state.destroyed {
365 delete_from_backend(&config, &state.id)
367 .await
368 .map_err(|e| worker::Error::RustError(format!("session delete failed: {e}")))?;
369 push_cookie(format!(
371 "{}=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0",
372 config.cookie_name
373 ));
374 return Ok(());
375 }
376
377 if state.dirty || state.is_new {
378 save_to_backend(&config, &state.id, &state.data)
380 .await
381 .map_err(|e| worker::Error::RustError(format!("session save failed: {e}")))?;
382 push_cookie(format!(
384 "{}={}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age={}",
385 config.cookie_name, state.id, config.max_age_secs
386 ));
387 }
388
389 SESSION_CONFIG.with(|cell| *cell.borrow_mut() = None);
391
392 Ok(())
393}
394
395async fn load_from_backend(
400 config: &SessionConfig,
401 session_id: &str,
402) -> Result<Option<HashMap<String, serde_json::Value>>, CfError> {
403 match &config.backend {
404 SessionBackend::Kv { binding } => load_from_kv(binding, session_id).await,
405 SessionBackend::D1 { binding, table } => load_from_d1(binding, table, session_id).await,
406 }
407}
408
409async fn save_to_backend(
410 config: &SessionConfig,
411 session_id: &str,
412 data: &HashMap<String, serde_json::Value>,
413) -> Result<(), CfError> {
414 match &config.backend {
415 SessionBackend::Kv { binding } => {
416 save_to_kv(binding, session_id, data, config.max_age_secs).await
417 }
418 SessionBackend::D1 { binding, table } => {
419 save_to_d1(binding, table, session_id, data, config.max_age_secs).await
420 }
421 }
422}
423
424async fn delete_from_backend(config: &SessionConfig, session_id: &str) -> Result<(), CfError> {
425 match &config.backend {
426 SessionBackend::Kv { binding } => delete_from_kv(binding, session_id).await,
427 SessionBackend::D1 { binding, table } => delete_from_d1(binding, table, session_id).await,
428 }
429}
430
431fn session_key(session_id: &str) -> String {
436 format!("session:{session_id}")
437}
438
439async fn load_from_kv(
440 binding: &str,
441 session_id: &str,
442) -> Result<Option<HashMap<String, serde_json::Value>>, CfError> {
443 let kv = crate::bindings::kv(binding)?;
444 let key = session_key(session_id);
445
446 let text = kv.get(&key).text().await.cf()?;
447
448 match text {
449 Some(json) => {
450 let data: HashMap<String, serde_json::Value> = serde_json::from_str(&json).cf()?;
451 Ok(Some(data))
452 }
453 None => Ok(None),
454 }
455}
456
457async fn save_to_kv(
458 binding: &str,
459 session_id: &str,
460 data: &HashMap<String, serde_json::Value>,
461 max_age_secs: u64,
462) -> Result<(), CfError> {
463 let kv = crate::bindings::kv(binding)?;
464 let key = session_key(session_id);
465 let json = serde_json::to_string(data).cf()?;
466
467 kv.put(&key, &json)
468 .cf()?
469 .expiration_ttl(max_age_secs)
470 .execute()
471 .await
472 .cf()?;
473
474 Ok(())
475}
476
477async fn delete_from_kv(binding: &str, session_id: &str) -> Result<(), CfError> {
478 let kv = crate::bindings::kv(binding)?;
479 let key = session_key(session_id);
480
481 kv.delete(&key).await.cf()?;
482
483 Ok(())
484}
485
486async fn load_from_d1(
491 binding: &str,
492 table: &str,
493 session_id: &str,
494) -> Result<Option<HashMap<String, serde_json::Value>>, CfError> {
495 let db = crate::bindings::d1(binding)?;
496 let key = session_key(session_id);
497
498 let result = db
499 .prepare(format!(
500 "SELECT data FROM {table} WHERE id = ? AND expires_at > unixepoch()"
501 ))
502 .bind(&[key.into()])
503 .map_err(|e| CfError(worker::Error::RustError(format!("D1 bind failed: {e}"))))?
504 .first::<serde_json::Value>(None)
505 .await
506 .cf()?;
507
508 match result {
509 Some(row) => {
510 let data_str = row
512 .get("data")
513 .and_then(|v| v.as_str())
514 .ok_or_else(|| {
515 CfError(worker::Error::RustError(
516 "session D1 row missing 'data' column".into(),
517 ))
518 })?;
519 let data: HashMap<String, serde_json::Value> =
520 serde_json::from_str(data_str).cf()?;
521 Ok(Some(data))
522 }
523 None => Ok(None),
524 }
525}
526
527async fn save_to_d1(
528 binding: &str,
529 table: &str,
530 session_id: &str,
531 data: &HashMap<String, serde_json::Value>,
532 max_age_secs: u64,
533) -> Result<(), CfError> {
534 let db = crate::bindings::d1(binding)?;
535 let key = session_key(session_id);
536 let json = serde_json::to_string(data).cf()?;
537
538 db.prepare(format!(
539 "INSERT OR REPLACE INTO {table} (id, data, expires_at) VALUES (?, ?, unixepoch() + ?)"
540 ))
541 .bind(&[key.into(), json.into(), max_age_secs.into()])
542 .map_err(|e| CfError(worker::Error::RustError(format!("D1 bind failed: {e}"))))?
543 .run()
544 .await
545 .cf()?;
546
547 Ok(())
548}
549
550async fn delete_from_d1(binding: &str, table: &str, session_id: &str) -> Result<(), CfError> {
551 let db = crate::bindings::d1(binding)?;
552 let key = session_key(session_id);
553
554 db.prepare(format!("DELETE FROM {table} WHERE id = ?"))
555 .bind(&[key.into()])
556 .map_err(|e| CfError(worker::Error::RustError(format!("D1 bind failed: {e}"))))?
557 .run()
558 .await
559 .cf()?;
560
561 Ok(())
562}