diff --git a/.claude/skills/controller-skill/skill.md b/.claude/skills/controller-skill/skill.md index bf8a12d..eb8b3a8 100644 --- a/.claude/skills/controller-skill/skill.md +++ b/.claude/skills/controller-skill/skill.md @@ -55,6 +55,11 @@ Generate a keypair and authorize a new session in a single step. "account": { "type": "string", "description": "Cartridge username to authorize the session for. Verifies the account exists and displays the resolved address. Also isolates session storage per account." + }, + "expires": { + "type": "string", + "description": "Session expiration duration (e.g., '1min', '1hr', '1day', '7days', '1week', '1year'). Plurals supported for day/week/year. Defaults to '7days'.", + "default": "7days" } } } @@ -72,6 +77,11 @@ controller session auth --preset loot-survivor --chain-id SN_MAIN --json controller session auth --preset loot-survivor --chain-id SN_MAIN --account shinobi --json ``` +**Example (custom expiration):** +```bash +controller session auth --preset loot-survivor --chain-id SN_MAIN --expires 1hr --json +``` + **Example (policy file):** ```bash controller session auth --file policy.json --json diff --git a/LLM_USAGE.md b/LLM_USAGE.md index 8203e82..87453de 100644 --- a/LLM_USAGE.md +++ b/LLM_USAGE.md @@ -93,6 +93,8 @@ The `session auth` command combines keypair generation and session registration **`--account` flag:** Use `--account ` to authorize a session for a specific Cartridge account. The CLI will verify the username exists and resolve it to a controller address before proceeding. This also isolates session storage per account, enabling multiple concurrent sessions. +**`--expires` flag:** Use `--expires ` to set the session expiration. Accepts human-readable durations: `1min`, `1hr`, `1day`, `7days`, `1week`, `1year` (plurals supported for day/week/year). Defaults to `7days`. + #### Option A: Use a Preset (Recommended) For popular games/apps, use a preset from [cartridge-gg/presets](https://github.com/cartridge-gg/presets/tree/main/configs): @@ -113,6 +115,15 @@ controller session auth \ --json ``` +**With custom expiration:** +```bash +controller session auth \ + --preset loot-survivor \ + --chain-id SN_MAIN \ + --expires 1hr \ + --json +``` + Available presets: loot-survivor, influence, realms, pistols, dope-wars, and more. #### Option B: Use a Local Policy File diff --git a/README.md b/README.md index 8837eeb..dd463e8 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ Or use a preset for popular games/apps: controller session auth --preset loot-survivor --chain-id SN_MAIN ``` +Use `--expires` to set session duration (default: `7days`). Accepts: `1min`, `1hr`, `1day`, `7days`, `1week`, `1year`. + This generates a new keypair, creates an authorization URL, and automatically polls until you authorize in the browser and stores the session. ### 2. Execute Transactions diff --git a/src/commands/session/authorize.rs b/src/commands/session/authorize.rs index 04a5bf6..a38e871 100644 --- a/src/commands/session/authorize.rs +++ b/src/commands/session/authorize.rs @@ -11,6 +11,7 @@ use account_sdk::storage::{ use serde::{Deserialize, Serialize}; use starknet::signers::SigningKey; use std::fmt::Display; +use std::time::{SystemTime, UNIX_EPOCH}; use url::Url; #[derive(Serialize, Deserialize)] @@ -49,12 +50,47 @@ fn default_authorized() -> bool { true } +/// Parse a duration string like "1min", "1hr", "7days", "1week", "1year" +/// and return the corresponding unix timestamp (now + duration). +fn parse_expiration(duration: &str) -> Result { + let duration = duration.trim().to_lowercase(); + + let (num_str, unit) = duration + .find(|c: char| c.is_alphabetic()) + .map(|i| (&duration[..i], &duration[i..])) + .ok_or_else(|| CliError::InvalidInput("Invalid duration format".to_string()))?; + + let num: u64 = num_str + .parse() + .map_err(|_| CliError::InvalidInput(format!("Invalid number in duration: {num_str}")))?; + + let seconds = match unit { + "min" | "mins" => num * 60, + "hr" | "hrs" => num * 3600, + "day" | "days" => num * 86400, + "week" | "weeks" => num * 604800, + "year" | "years" => num * 31536000, + _ => { + return Err(CliError::InvalidInput(format!( + "Unknown duration unit: '{unit}'. Use min, hr, day/days, week/weeks, or year/years" + ))) + } + }; + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| CliError::InvalidInput(format!("System time error: {e}")))?; + + Ok(now.as_secs() + seconds) +} + #[derive(Serialize)] pub struct AuthorizeOutput { pub authorization_url: String, #[serde(skip_serializing_if = "Option::is_none")] pub short_url: Option, pub public_key: String, + pub expires: String, pub message: String, } @@ -92,6 +128,7 @@ pub async fn execute( rpc_url: Option, overwrite: bool, account: Option<&str>, + expires: &str, ) -> Result<()> { // Validate that either preset or file is provided if preset.is_none() && file.is_none() { @@ -305,6 +342,7 @@ pub async fn execute( formatter.info(&format!( "Policies loaded: {total_contracts} contracts, {total_entrypoints} entrypoints" )); + formatter.info(&format!("Session expiration: {expires}")); // Convert to the format expected by the keychain let mut policies = serde_json::json!({ @@ -415,6 +453,9 @@ pub async fn execute( } }; + // Parse expiration duration + let expires_at = parse_expiration(expires)?; + // Build the authorization URL let mut url = Url::parse(&format!("{}/session", config.session.keychain_url)) .map_err(|e| CliError::InvalidInput(format!("Invalid keychain URL: {e}")))?; @@ -426,6 +467,7 @@ pub async fn execute( .append_pair("redirect_uri", "https://x.cartridge.gg") .append_pair("policies", &policies_json) .append_pair("rpc_url", effective_rpc_url) + .append_pair("expires_at", &expires_at.to_string()) .append_pair("mode", "cli"); // Tell keychain this is CLI mode (don't include session data in redirect) if let Some(name) = account { @@ -448,6 +490,7 @@ pub async fn execute( authorization_url: authorization_url.clone(), short_url: short_url.clone(), public_key: public_key.clone(), + expires: expires.to_string(), message: "Open this URL in your browser to authorize the session. Waiting for authorization..." .to_string(), diff --git a/src/main.rs b/src/main.rs index 6cf2321..4f70a29 100644 --- a/src/main.rs +++ b/src/main.rs @@ -385,6 +385,11 @@ enum SessionCommands { /// Overwrite existing session without confirmation #[arg(long)] overwrite: bool, + + /// Session expiration duration (e.g., '1min', '1hr', '1day', '7days', '1week', '1year') + /// Defaults to 7days + #[arg(long, default_value = "7days")] + expires: String, }, /// Display current session status and information @@ -455,6 +460,7 @@ async fn main() { chain_id, rpc_url, overwrite, + expires, } => { commands::session::authorize::execute( &config, @@ -465,6 +471,7 @@ async fn main() { rpc_url, overwrite, account.as_deref(), + &expires, ) .await }