Skip to content

Add sandbox JWT support#78

Merged
pthurlow merged 3 commits into
mainfrom
feat/sandbox-jwt
May 12, 2026
Merged

Add sandbox JWT support#78
pthurlow merged 3 commits into
mainfrom
feat/sandbox-jwt

Conversation

@pthurlow
Copy link
Copy Markdown
Collaborator

No description provided.

@sentry
Copy link
Copy Markdown

sentry Bot commented May 12, 2026

Codecov Report

❌ Patch coverage is 67.31707% with 134 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/sandbox_session.rs 77.72% 51 Missing ⚠️
src/sandbox.rs 38.46% 32 Missing ⚠️
src/auth.rs 31.81% 30 Missing ⚠️
src/api.rs 52.00% 12 Missing ⚠️
src/main.rs 50.00% 7 Missing ⚠️
src/util.rs 95.65% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread src/sandbox_session.rs
Comment on lines +206 to +223
pub fn refresh_from_env(api_url: &str) -> Option<String> {
let current = std::env::var("HOTDATA_SANDBOX_TOKEN").ok()?;
let needs_refresh = match jwt_exp(&current) {
Some(exp) => exp.saturating_sub(REFRESH_LEEWAY_SECONDS) <= now_unix(),
None => true,
};
if !needs_refresh {
return Some(current);
}
let rt = std::env::var("HOTDATA_SANDBOX_REFRESH_TOKEN").ok()?;
if rt.is_empty() {
return Some(current);
}
match refresh(api_url, &rt) {
Ok(new_session) => Some(new_session.access_token),
Err(_) => Some(current),
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: (not blocking) The "HOTDATA_SANDBOX_TOKEN is empty" error in api.rs:69 is effectively unreachable. std::env::var(...).is_ok() is true when the var is set to an empty string, but refresh_from_env happily returns Some("") for that case (line 207's ? only triggers on NotPresent, not empty). The downstream effect is the client issues Authorization: Bearer and the user sees a confusing 401 instead of the intended message.

A short guard at the top of refresh_from_env would tighten this up:

let current = std::env::var("HOTDATA_SANDBOX_TOKEN").ok()?;
if current.is_empty() {
    return None;
}

This also matches the early-empty check already in sandbox_token_in_use (line 189).

Comment thread src/sandbox.rs
Comment on lines +200 to +204
"json" => println!("{}", serde_json::json!({"public_id": sandbox_id})),
"yaml" => print!(
"{}",
serde_yaml::to_string(&serde_json::json!({"public_id": sandbox_id})).unwrap()
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: (not blocking) The output shape from sandbox new --output json / --output yaml regresses here — previously it was a full Sandbox object (public_id, name, markdown, created_at, updated_at), now it's just {"public_id": "..."}. Any caller doing hotdata sandbox new -o json | jq .name (or the timestamps) will break silently.

If /auth/sandbox can't return the full object, a follow-up api.get(&format!("/sandboxes/{sandbox_id}")) to render the existing shape would preserve the contract. The table branch on line 207 has the same issue — name echoes the flag rather than the server's stored name, and created_at/updated_at are gone.

Comment thread src/main.rs
Comment on lines +123 to +126
// Register before `Cli::parse`, since `--help` / `--version` exit
// from inside the parser. Safety: `atexit` is async-signal-safe;
// the callback only reads env vars / files and writes to stderr.
unsafe { atexit(print_sandbox_footer) };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: (not blocking) The async-signal-safety claim is misleading — atexit callbacks aren't invoked from signal context, so async-signal-safety isn't the relevant property here. And the callback as written isn't AS-safe anyway (eprintln! takes a lock + allocates, and fs::read_to_string allocates and syscalls).

What's actually being justified is that (a) the function pointer is valid for the process lifetime and (b) the callback is extern "C" and won't unwind into C. A short rewrite, e.g. "Safety: callback is extern "C" and never unwinds; lives for the program's lifetime" would be more accurate.

claude[bot]
claude Bot previously approved these changes May 12, 2026
@pthurlow pthurlow merged commit 7eca1fb into main May 12, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant