Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .claude/skills/controller-skill/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions LLM_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ The `session auth` command combines keypair generation and session registration

**`--account` flag:** Use `--account <username>` 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 <duration>` 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):
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions src/commands/session/authorize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<u64> {
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<String>,
pub public_key: String,
pub expires: String,
pub message: String,
}

Expand Down Expand Up @@ -92,6 +128,7 @@ pub async fn execute(
rpc_url: Option<String>,
overwrite: bool,
account: Option<&str>,
expires: &str,
) -> Result<()> {
// Validate that either preset or file is provided
if preset.is_none() && file.is_none() {
Expand Down Expand Up @@ -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!({
Expand Down Expand Up @@ -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}")))?;
Expand All @@ -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 {
Expand All @@ -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(),
Expand Down
7 changes: 7 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -455,6 +460,7 @@ async fn main() {
chain_id,
rpc_url,
overwrite,
expires,
} => {
commands::session::authorize::execute(
&config,
Expand All @@ -465,6 +471,7 @@ async fn main() {
rpc_url,
overwrite,
account.as_deref(),
&expires,
)
.await
}
Expand Down