Merge remote-tracking branch 'origin/develop' into ticket/t-768-subject-grants

# Conflicts:
#	crates/worker-runtime/src/worker_backend.rs
#	openapi/server-api.json
#	web/workspace/src/lib/generated/companion-api.ts
#	web/workspace/src/lib/generated/repository-api.ts
#	web/workspace/src/lib/generated/runtime-api.ts
#	web/workspace/src/lib/generated/ticket-api.ts
#	web/workspace/src/lib/generated/worker-launch-api.ts
This commit is contained in:
2026-10-20 06:24:11 +09:00
116 changed files with 9151 additions and 2358 deletions
+366 -8
View File
@@ -91,11 +91,38 @@ pub fn parse_bearer(headers: &HeaderMap) -> Option<String> {
}
pub fn parse_cookie(headers: &HeaderMap, cookie_name: &str) -> Option<String> {
let cookie_header = headers.get(header::COOKIE)?.to_str().ok()?;
cookie_header.split(';').find_map(|pair| {
let (name, value) = pair.trim().split_once('=')?;
(name == cookie_name && !value.is_empty()).then(|| value.to_string())
})
unique_cookie(headers, cookie_name)
.ok()
.flatten()
.filter(|value| !value.is_empty())
}
// Cookie has no path/domain metadata on requests. Duplicates (including identical values
// and multiple Cookie headers) cannot be disambiguated safely after cookie tossing.
fn unique_cookie(
headers: &HeaderMap,
cookie_name: &str,
) -> std::result::Result<Option<String>, ()> {
let mut found = None;
for header in headers.get_all(header::COOKIE) {
let header = header.to_str().map_err(|_| ())?;
for pair in header.split(';') {
let pair = pair.trim();
let Some((name, value)) = pair.split_once('=') else {
if pair == cookie_name {
return Err(());
}
continue;
};
if name == cookie_name {
if found.is_some() {
return Err(());
}
found = Some(value.to_owned());
}
}
}
Ok(found)
}
pub async fn resolve_request_actor(
@@ -121,6 +148,14 @@ pub(crate) fn resolve_request_actor_sync(
bearer: Option<String>,
session: Option<String>,
) -> Result<Option<RequestActor>> {
Ok(resolve_credentials_sync(store, bearer, session)?.map(|(actor, _)| actor))
}
fn resolve_credentials_sync(
store: &dyn ControlPlaneStore,
bearer: Option<String>,
session: Option<String>,
) -> Result<Option<(RequestActor, Option<String>)>> {
if let Some(token) = bearer {
let token_hash = token_hash(&token);
if let Some(api_token) = store.resolve_api_token(&token_hash)? {
@@ -132,7 +167,10 @@ pub(crate) fn resolve_request_actor_sync(
return Ok(None);
}
store.mark_api_token_used(&token_hash, &now_rfc3339())?;
return actor_for_user(store, &api_token.user_id, ActorAuthMethod::ApiToken);
return Ok(
actor_for_user(store, &api_token.user_id, ActorAuthMethod::ApiToken)?
.map(|actor| (actor, None)),
);
}
}
@@ -141,13 +179,95 @@ pub(crate) fn resolve_request_actor_sync(
if is_expired(&session.expires_at) {
return Ok(None);
}
return actor_for_user(store, &session.user_id, ActorAuthMethod::BrowserSession);
return Ok(
actor_for_user(store, &session.user_id, ActorAuthMethod::BrowserSession)?
.map(|actor| (actor, Some(session.expires_at))),
);
}
}
Ok(None)
}
// This marker is a non-authenticating reference to the existing non-revoked session row.
// It outlives the auth cookie so Max-Age expiry remains distinguishable from a new browser.
// No second session store is introduced; clearing all cookies necessarily loses this evidence.
// Keep the established 14-day authentication lifetime. The additional week is only a bounded
// return-navigation window (including a week away); it never extends authentication. The store
// reclaims expired rows past this same boundary on subsequent session issuance. Neither whoami
// nor other reads refresh the marker or the row's absolute expiry.
pub const BROWSER_SESSION_SECONDS: i64 = 14 * 24 * 60 * 60;
pub const SESSION_EXPIRY_GRACE_SECONDS: i64 = 7 * 24 * 60 * 60;
pub fn session_marker_cookie_name(cookie_name: &str) -> String {
format!("{cookie_name}_expiry")
}
pub async fn resolve_whoami(
store: std::sync::Arc<dyn ControlPlaneStore>,
headers: &HeaderMap,
cookie_name: &str,
) -> Result<server_api::WhoamiResponse> {
let bearer = parse_bearer(headers);
// Even an empty/invalid auth cookie takes precedence over an older marker.
// An ambiguous primary is invalid, not absent; never fall back to an older marker.
let session = unique_cookie(headers, cookie_name).unwrap_or_else(|()| Some(String::new()));
let marker = parse_cookie(headers, &session_marker_cookie_name(cookie_name));
let executor = store.db_executor();
executor
.run(move || resolve_whoami_sync(store.as_ref(), bearer, session, marker))
.await
}
fn resolve_whoami_sync(
store: &dyn ControlPlaneStore,
bearer: Option<String>,
session_token: Option<String>,
marker: Option<String>,
) -> Result<server_api::WhoamiResponse> {
use server_api::{BrowserSessionState, WhoamiResponse};
let anonymous = || WhoamiResponse {
actor: None,
session: BrowserSessionState::Anonymous {},
};
// Preserve the existing API-token precedence and fallback rules, including expired tokens.
if let Some((actor, expires_at)) =
resolve_credentials_sync(store, bearer, session_token.clone())?
{
return Ok(WhoamiResponse {
actor: Some(actor),
session: BrowserSessionState::Authenticated { expires_at },
});
}
let hash = match session_token {
Some(token) => token_hash(&token),
None => match marker {
Some(hash) => hash,
None => return Ok(anonymous()),
},
};
let Some(session) = store.resolve_browser_session(&hash)? else {
return Ok(anonymous());
};
if store.get_user(&session.user_id)?.is_none() {
return Ok(anonymous());
}
if within_session_expiry_grace(&session.expires_at, Utc::now()) {
return Ok(WhoamiResponse {
actor: None,
session: BrowserSessionState::Expired {},
});
}
// A marker for a still-valid session cannot restore authentication after partial deletion.
Ok(anonymous())
}
fn within_session_expiry_grace(expires_at: &str, now: DateTime<Utc>) -> bool {
DateTime::parse_from_rfc3339(expires_at).is_ok_and(|expires_at| {
expires_at <= now && now < expires_at + Duration::seconds(SESSION_EXPIRY_GRACE_SECONDS)
})
}
fn actor_for_user<S: ControlPlaneStore + ?Sized>(
store: &S,
user_id: &str,
@@ -274,6 +394,13 @@ mod tests {
.unwrap();
assert_eq!(actor.user_id, "user-auth");
assert_eq!(actor.auth_method, ActorAuthMethod::ApiToken);
let whoami = resolve_whoami(store.clone(), &headers, "session")
.await
.unwrap();
assert_eq!(
whoami.session,
server_api::BrowserSessionState::Authenticated { expires_at: None }
);
assert!(
store
.resolve_api_token(&hash)
@@ -363,20 +490,119 @@ mod tests {
assert!(futures::poll!(queued.as_mut()).is_pending());
let mut headers = HeaderMap::new();
headers.insert(header::COOKIE, "session=session-secret".parse().unwrap());
let actor_result = tokio::time::timeout(
std::time::Duration::from_secs(1),
resolve_request_actor(store.clone(), &headers, "session"),
)
.await;
let result = tokio::time::timeout(
std::time::Duration::from_secs(1),
resolve_request_actor(store, &headers, "session"),
resolve_whoami(store, &headers, "session"),
)
.await;
release.send(()).unwrap();
blocker.await.unwrap().unwrap();
queued.await.unwrap();
assert!(matches!(
actor_result.expect("actor lookup rejects capacity"),
Err(Error::DbCapacityExceeded)
));
assert!(matches!(
result.expect("capacity rejection must not wait for queued SQL"),
Err(Error::DbCapacityExceeded)
));
}
#[tokio::test]
async fn whoami_classifies_only_live_store_backed_session_expiry() {
use server_api::BrowserSessionState;
let store = seeded_store();
for (secret, offset, revoked) in [
("recent", -60, false),
("old", -SESSION_EXPIRY_GRACE_SECONDS - 60, false),
("revoked", -60, true),
] {
store
.create_browser_session(&BrowserSessionRecord {
session_id: format!("session-{secret}"),
token_hash: token_hash(secret),
user_id: "user-auth".into(),
created_at: "1".into(),
expires_at: rfc3339_after(Duration::seconds(offset)),
revoked_at: revoked.then(|| now_rfc3339()),
})
.unwrap();
}
let cases = [
(String::new(), BrowserSessionState::Anonymous {}),
("session=unknown".into(), BrowserSessionState::Anonymous {}),
("session=recent".into(), BrowserSessionState::Expired {}),
(
format!("session_expiry={}", token_hash("recent")),
BrowserSessionState::Expired {},
),
(
format!("session_expiry={}", token_hash("session-secret")),
BrowserSessionState::Anonymous {},
),
(
"session_expiry=unknown".into(),
BrowserSessionState::Anonymous {},
),
(
"session_expiry=recent".into(),
BrowserSessionState::Anonymous {},
),
(
format!("session=unknown; session_expiry={}", token_hash("recent")),
BrowserSessionState::Anonymous {},
),
(
format!("session=; session_expiry={}", token_hash("recent")),
BrowserSessionState::Anonymous {},
),
("session=revoked".into(), BrowserSessionState::Anonymous {}),
(
format!("session_expiry={}", token_hash("revoked")),
BrowserSessionState::Anonymous {},
),
("session=old".into(), BrowserSessionState::Anonymous {}),
(
format!("session_expiry={}", token_hash("old")),
BrowserSessionState::Anonymous {},
),
(
format!(
"session=session-secret; session_expiry={}",
token_hash("recent")
),
BrowserSessionState::Authenticated {
expires_at: Some("9999-01-01T00:00:00Z".into()),
},
),
];
for (cookie, expected) in cases {
let mut headers = HeaderMap::new();
headers.insert(header::COOKIE, cookie.parse().unwrap());
let response = resolve_whoami(store.clone(), &headers, "session")
.await
.unwrap();
assert_eq!(response.session, expected, "cookie: {cookie}");
assert_eq!(
response.actor.is_some(),
matches!(expected, BrowserSessionState::Authenticated { .. })
);
if !matches!(expected, BrowserSessionState::Authenticated { .. }) {
assert!(
resolve_request_actor(store.clone(), &headers, "session")
.await
.unwrap()
.is_none()
);
}
}
}
#[test]
fn user_code_uses_the_uuid_random_tail() {
let id = Uuid::parse_str("01234567-89ab-7cde-8123-456789abcdef").expect("UUID");
@@ -395,4 +621,136 @@ mod tests {
);
assert_eq!(code, code.to_ascii_uppercase());
}
#[test]
fn expiry_grace_includes_expiry_but_excludes_its_absolute_end() {
let expiry = DateTime::parse_from_rfc3339("2026-01-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc);
for (offset, expected) in [
(-1, false),
(0, true),
(SESSION_EXPIRY_GRACE_SECONDS - 1, true),
(SESSION_EXPIRY_GRACE_SECONDS, false),
] {
assert_eq!(
within_session_expiry_grace(
&expiry.to_rfc3339(),
expiry + Duration::seconds(offset)
),
expected
);
}
assert!(!within_session_expiry_grace("invalid", expiry));
}
#[tokio::test]
async fn duplicate_cookies_never_select_an_actor_or_promote_an_ambiguous_marker() {
use server_api::BrowserSessionState;
let store = seeded_store();
let expired_hash = token_hash("expired-duplicate");
store
.create_browser_session(&BrowserSessionRecord {
session_id: "expired-duplicate".into(),
token_hash: expired_hash.clone(),
user_id: "user-auth".into(),
created_at: now_rfc3339(),
expires_at: rfc3339_after(Duration::seconds(-60)),
revoked_at: None,
})
.unwrap();
for cookies in [
vec!["session=session-secret; session=unknown".to_owned()],
vec!["session=session-secret; session=session-secret".to_owned()],
vec![
"session=session-secret".to_owned(),
"session=unknown".to_owned(),
],
vec![format!(
"session=; session=session-secret; session_expiry={expired_hash}"
)],
vec![format!(
"session_expiry={expired_hash}; session_expiry={expired_hash}"
)],
vec![format!("session; session_expiry={expired_hash}")],
] {
let mut headers = HeaderMap::new();
for cookie in cookies {
headers.append(header::COOKIE, cookie.parse().unwrap());
}
assert!(
resolve_request_actor(store.clone(), &headers, "session")
.await
.unwrap()
.is_none()
);
assert_eq!(
resolve_whoami(store.clone(), &headers, "session")
.await
.unwrap()
.session,
BrowserSessionState::Anonymous {}
);
}
let mut headers = HeaderMap::new();
headers.insert(
header::COOKIE,
format!("session=session-secret; session_expiry={expired_hash}")
.parse()
.unwrap(),
);
assert!(matches!(
resolve_whoami(store, &headers, "session")
.await
.unwrap()
.session,
BrowserSessionState::Authenticated { .. }
));
}
#[test]
fn session_issuance_reclaims_only_rows_beyond_recovery_grace() {
let store = seeded_store();
let record = |secret: &str, offset| BrowserSessionRecord {
session_id: secret.into(),
token_hash: token_hash(secret),
user_id: "user-auth".into(),
created_at: now_rfc3339(),
expires_at: rfc3339_after(Duration::seconds(offset)),
revoked_at: None,
};
store
.create_browser_session(&record("past-grace", -SESSION_EXPIRY_GRACE_SECONDS - 60))
.unwrap();
assert!(
store
.resolve_browser_session(&token_hash("past-grace"))
.unwrap()
.is_some()
);
store
.create_browser_session(&record("within-grace", -SESSION_EXPIRY_GRACE_SECONDS + 60))
.unwrap();
assert!(
store
.resolve_browser_session(&token_hash("past-grace"))
.unwrap()
.is_none()
);
store
.create_browser_session(&record("new-session", BROWSER_SESSION_SECONDS))
.unwrap();
assert!(
store
.resolve_browser_session(&token_hash("within-grace"))
.unwrap()
.is_some()
);
assert!(
store
.resolve_browser_session(&token_hash("new-session"))
.unwrap()
.is_some()
);
}
}
File diff suppressed because it is too large Load Diff
@@ -473,11 +473,10 @@ async fn generated_workdir_provider_errors_preserve_response_and_log_details() {
));
// Seed an already registered command, without launching a real process.
let provider_handle = CommandHandle("logging-command".to_string());
session
.active_commands
.lock()
.await
.insert(provider_handle.0.clone(), provider_handle.clone());
session.active_commands.lock().await.insert(
provider_handle.0.clone(),
Arc::new(ExternalProviderCommandState::new(provider_handle.clone())),
);
let handle = fixture
.api
.workdir_sessions
@@ -733,7 +732,7 @@ async fn self_workdir_provider_refusals_log_operation_identity_without_request_t
Reason::ScopedCapabilityDenied,
),
(
json!({"operation": "command_start", "request": {"command": secret, "timeout_secs": 1, "output_limit": 1024, "cwd": "", "tool_call_id": secret}}),
json!({"operation": "command_start", "request": {"command": secret, "timeout_secs": 1, "transport_byte_budget": 1048576, "cwd": "", "tool_call_id": secret}}),
"command_start",
"denied",
Reason::ScopedCapabilityDenied,
@@ -955,8 +954,8 @@ async fn self_workdir_post_start_registration_failure_does_not_claim_pre_dispatc
serde_json::to_vec(&json!({
"target_workdir": "checkout",
"operation": {"operation": "command_start", "request": {
"command": "body-secret", "timeout_secs": 1, "output_limit": 1024,
"cwd": "", "spill_dir": null, "tool_call_id": "body-secret"
"command": "body-secret", "timeout_secs": 1, "transport_byte_budget": 1048576,
"cwd": "", "tool_call_id": "body-secret"
}}
}))
.unwrap(),
@@ -1084,8 +1083,8 @@ async fn saturated_host_before_command_start_returns_availability_without_a_prov
serde_json::to_vec(&json!({
"target_workdir": "checkout",
"operation": {"operation": "command_start", "request": {
"command": "not-dispatched", "timeout_secs": 1, "output_limit": 1024,
"cwd": "", "spill_dir": null, "tool_call_id": "not-dispatched"
"command": "not-dispatched", "timeout_secs": 1, "transport_byte_budget": 1048576,
"cwd": "", "tool_call_id": "not-dispatched"
}}
}))
.unwrap(),
@@ -0,0 +1,223 @@
//! Provider-bridge lifecycle tests. The production bridge runs against a controlled
//! provider boundary; the websocket/CLI-pump test in the parent module covers real byte transport.
use super::*;
use workdir::WorkdirSession;
fn command_fixture() -> (
Arc<ExternalProviderWorkdirSession>,
tokio::sync::mpsc::Receiver<ExternalProviderCommand>,
) {
let (sender, receiver) = tokio::sync::mpsc::channel(16);
let session = Arc::new(ExternalProviderWorkdirSession::new(
Arc::new(ExternalProviderConnection {
grant_id: "command-grant".into(),
workdir_id: "command-workdir".into(),
provider_instance_id: "command-provider".into(),
generation: 1,
expires_at: None,
capabilities: workdir::WorkdirSessionCapabilities::COMMAND_ONLY,
admission: Arc::new(tokio::sync::Semaphore::new(1)),
shutdown_confirmed: Arc::new(std::sync::atomic::AtomicBool::new(false)),
sender,
}),
None,
));
(session, receiver)
}
fn page(cursor: u64, bytes: &[u8], terminal: bool) -> workdir::CommandOutput {
workdir::CommandOutput {
chunks: if bytes.is_empty() {
Vec::new()
} else {
vec![workdir::CommandByteChunk {
sequence: cursor,
stream: workdir::CommandStream::Stdout,
offset: cursor,
bytes: bytes.to_vec(),
}]
},
next_cursor: cursor + bytes.len() as u64,
terminal: terminal.then_some(workdir::CommandTerminal {
status: workdir::CommandStatus::Cancelled,
exit_code: None,
stdout_bytes: cursor + bytes.len() as u64,
stderr_bytes: 0,
stdout_delivered_bytes: cursor + bytes.len() as u64,
stderr_delivered_bytes: 0,
truncation_reason: None,
}),
}
}
async fn receive(
receiver: &mut tokio::sync::mpsc::Receiver<ExternalProviderCommand>,
) -> (
WorkdirSessionOperation,
tokio::sync::oneshot::Sender<
std::result::Result<WorkdirSessionOperationResult, WorkdirTransportError>,
>,
) {
let command = tokio::time::timeout(std::time::Duration::from_secs(2), receiver.recv())
.await
.unwrap()
.unwrap();
let ExternalProviderCommand::Operation {
operation,
response,
..
} = command
else {
panic!("expected provider operation")
};
(operation, response)
}
#[tokio::test]
async fn close_cancels_waiting_reader_then_drains_from_latest_cursor() {
let (session, mut receiver) = command_fixture();
session.active_commands.lock().await.insert(
"external".into(),
Arc::new(ExternalProviderCommandState::new(CommandHandle(
"provider".into(),
))),
);
let reader = {
let session = session.clone();
tokio::spawn(async move {
session
.command_output(workdir::CommandOutputRequest {
handle: CommandHandle("external".into()),
cursor: 0,
limit: 3,
wait: true,
})
.await
})
};
let (_, response) = receive(&mut receiver).await;
response
.send(Ok(WorkdirSessionOperationResult::CommandOutput(page(
0, b"one", false,
))))
.unwrap();
assert_eq!(reader.await.unwrap().unwrap().next_cursor, 3);
let reader = {
let session = session.clone();
tokio::spawn(async move {
session
.command_output(workdir::CommandOutputRequest {
handle: CommandHandle("external".into()),
cursor: 3,
limit: 3,
wait: true,
})
.await
})
};
let (_, pending_read) = receive(&mut receiver).await;
let closer = {
let session = session.clone();
tokio::spawn(async move { session.close().await })
};
let (operation, response) = receive(&mut receiver).await;
assert!(
matches!(operation, WorkdirSessionOperation::CommandCancel(ref handle) if handle.0 == "provider")
);
response
.send(Ok(WorkdirSessionOperationResult::CommandCancel))
.unwrap();
pending_read
.send(Ok(WorkdirSessionOperationResult::CommandOutput(page(
3, b"two", false,
))))
.unwrap();
assert_eq!(reader.await.unwrap().unwrap().next_cursor, 6);
for (cursor, bytes, terminal) in [(6, &b"end"[..], false), (9, &b""[..], true)] {
let (operation, response) = receive(&mut receiver).await;
assert!(
matches!(operation, WorkdirSessionOperation::CommandOutput(ref request) if request.handle.0 == "provider" && request.cursor == cursor && request.wait)
);
response
.send(Ok(WorkdirSessionOperationResult::CommandOutput(page(
cursor, bytes, terminal,
))))
.unwrap();
}
tokio::time::timeout(std::time::Duration::from_secs(2), closer)
.await
.unwrap()
.unwrap()
.unwrap();
}
#[tokio::test]
async fn terminal_output_retains_same_binding_for_current_page_replay() {
let (session, mut receiver) = command_fixture();
session.active_commands.lock().await.insert(
"external".into(),
Arc::new(ExternalProviderCommandState::new(CommandHandle(
"provider".into(),
))),
);
for _ in 0..2 {
let reader = {
let session = session.clone();
tokio::spawn(async move {
session
.command_output(workdir::CommandOutputRequest {
handle: CommandHandle("external".into()),
cursor: 0,
limit: 3,
wait: true,
})
.await
})
};
let (operation, response) = receive(&mut receiver).await;
assert!(
matches!(operation, WorkdirSessionOperation::CommandOutput(ref request) if request.handle.0 == "provider" && request.cursor == 0)
);
response
.send(Ok(WorkdirSessionOperationResult::CommandOutput(page(
0, b"end", true,
))))
.unwrap();
assert_eq!(reader.await.unwrap().unwrap(), page(0, b"end", true));
}
assert_eq!(session.active_commands.lock().await.len(), 1);
}
#[tokio::test]
async fn terminal_registry_retention_is_bounded_and_excludes_active_commands() {
let (source, _) = command_fixture();
let worker = RuntimeWorkerRef::new("runtime", "worker");
let mut registry = WorkdirSessionRegistry::default();
let active = registry.register_command(
worker.clone(),
"checkout".into(),
source.clone(),
CommandHandle("active".into()),
);
let mut last = None;
for index in 0..MAX_RETAINED_TERMINAL_COMMANDS + 4 {
let handle = registry.register_command(
worker.clone(),
"checkout".into(),
source.clone(),
CommandHandle(format!("terminal-{index}")),
);
registry.mark_terminal(&worker, &handle);
last = Some(handle);
}
assert_eq!(registry.active_command_count(&worker), 1);
assert_eq!(registry.commands.len(), MAX_RETAINED_TERMINAL_COMMANDS + 1);
assert!(registry.command(&worker, "checkout", &active).is_some());
assert!(
registry
.command(&worker, "checkout", &last.unwrap())
.is_some()
);
}
@@ -466,7 +466,6 @@ impl AttentionWorkerFactory {
worker.with_notification_coalesce_delay(std::time::Duration::from_millis(10)),
worker::WorkerBootstrapLayout::RuntimeManagedRun {
run_dir: root.join(uuid::Uuid::now_v7().to_string()),
bash_output_dir: root.join("bash-output"),
},
worker::WorkerControllerTransport::InProcess,
)
@@ -799,7 +799,6 @@ impl DeliveryWorkerFactory {
worker::WorkerFilesystemAuthority::None,
worker::WorkerBootstrapLayout::RuntimeManagedRun {
run_dir: root.join(uuid::Uuid::now_v7().to_string()),
bash_output_dir: root.join("bash-output"),
},
worker::WorkerControllerTransport::InProcess,
)
+10
View File
@@ -7131,6 +7131,16 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
fn create_browser_session(&self, record: &BrowserSessionRecord) -> Result<()> {
self.with_immediate_transaction(|conn| {
activation::require_active(conn, &record.user_id)?;
// Retain the real row throughout the non-authenticating recovery window, not
// forever. Issuance amortizes reclamation without extending marker lifetime or
// introducing another store/scheduler. Inactive stores do not grow between issues.
let cutoff = (chrono::Utc::now()
- chrono::Duration::seconds(crate::auth::SESSION_EXPIRY_GRACE_SECONDS))
.to_rfc3339();
conn.execute(
"DELETE FROM browser_sessions WHERE julianday(expires_at) <= julianday(?1) OR julianday(expires_at) IS NULL",
params![cutoff],
)?;
conn.execute(
r#"INSERT INTO browser_sessions (session_id, token_hash, user_id, created_at, expires_at, revoked_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#,