//! Seeds a throwaway sqlite database with what `/api/session` needs to //! accept a session for a test DID, and prints a matching signed cookie. //! //! Not for production: this exists so a local dev instance (started by //! `scripts/dev-instance.sh`) can be driven as a signed-in player without a //! real ATProto OAuth login, which `VITE_LOCAL_AUTH=1`'s `devauth.ts` does //! not help with — it fakes the frontend's sign-in and never touches //! headquarters-api at all, so `session.rs`'s cookie check still sees no //! session. This mints a real one instead, with the real signing code //! (`session::SessionSigner`) rather than a hand-reimplemented copy of it — //! see `lib.rs` for why that distinction matters. //! //! `/api/session` checks more than the cookie: `has_oauth_session` must //! also find a row (`routes.rs`, "the cookie alone is not a session: the //! stored OAuth session is"). `value` there is never read for its content by //! anything this tool needs to satisfy, only its presence — see //! `Db::has_oauth_session` — so an empty JSON object is enough. //! //! Reads its configuration from the environment, matching `Config::from_env`'s //! own convention rather than introducing a CLI-flag style this crate does //! not otherwise use: //! //! DEV_SESSION_DB required - path to the target sqlite file. Created //! (with the real migrations, via Db::open) if it //! does not exist yet. //! SESSION_SECRET optional - must match what the target server was //! started with, or the cookie this prints will not //! verify. Defaults to this codebase's own test //! fixture secret (session.rs's test module), a //! fixed, published, non-sensitive value that only //! ever signs throwaway local sessions. //! DEV_SESSION_DID optional - defaults to a fixed, obviously-fake DID. //! DEV_SESSION_HANDLE optional - if set, also seeds an `account` row //! with this handle, checked-in-full so nothing //! tries to refresh it from the network. //! //! Prints exactly one line to stdout: the cookie value, nothing else, so a //! caller can capture it with `$(...)`. Everything else goes to stderr. const DEV_TEST_SECRET: &str = "0123456789abcdef0123456789abcdef"; const DEV_DEFAULT_DID: &str = "did:plc:devsession0000000000000000"; #[tokio::main] async fn main() { let db_path = std::env::var("DEV_SESSION_DB").unwrap_or_else(|_| { eprintln!("dev_session: DEV_SESSION_DB is required (path to a sqlite file)"); std::process::exit(1); }); let secret = std::env::var("SESSION_SECRET").unwrap_or_else(|_| DEV_TEST_SECRET.to_owned()); let did = std::env::var("DEV_SESSION_DID").unwrap_or_else(|_| DEV_DEFAULT_DID.to_owned()); let handle = std::env::var("DEV_SESSION_HANDLE").ok(); let db = match headquarters_api::db::Db::open(std::path::Path::new(&db_path)) { Ok(db) => db, Err(e) => { eprintln!("dev_session: cannot open {db_path}: {e}"); std::process::exit(1); } }; let write_did = did.clone(); let result = db .call(move |conn| { conn.execute( "INSERT INTO oauth_session (did, value) VALUES (?1, '{}') ON CONFLICT (did) DO UPDATE SET updated_at = unixepoch()", [&write_did], )?; Ok(()) }) .await; if let Err(e) = result { eprintln!("dev_session: could not seed oauth_session: {e}"); std::process::exit(1); } if let Some(handle) = &handle { let write_did = did.clone(); let write_handle = handle.clone(); let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("clock before 1970") .as_secs() as i64; let result = db .call(move |conn| { conn.execute( "INSERT INTO account (did, handle, verified_at, checked_at) VALUES (?1, ?2, ?3, ?3) ON CONFLICT (did) DO UPDATE SET handle = ?2, checked_at = ?3", rusqlite::params![write_did, write_handle, now], )?; Ok(()) }) .await; if let Err(e) = result { eprintln!("dev_session: could not seed account: {e}"); std::process::exit(1); } } let signer = headquarters_api::session::SessionSigner::new(secret.into_bytes(), false, None); let cookie = signer.issue(&did); eprintln!( "dev_session: seeded {did}{} in {db_path}", handle.map(|h| format!(" (handle {h})")).unwrap_or_default() ); println!("{}", cookie.value()); }