1use super::{Error, Result, Settled, access_token, identify, oauth_of, purge};
10use crate::api::Owner;
11use crate::context::Context;
12use crate::state::{Account, Park, State};
13use crate::{claude, home, park, state, store};
14use serde_json::{Value, json};
15use std::fs::{File, OpenOptions, TryLockError};
16use std::os::unix::fs::OpenOptionsExt;
17use std::path::PathBuf;
18use std::process::Command;
19
20pub enum Enrolled {
21 Current { email: String },
23 SignedIn { email: String },
25 Renewed { email: String },
27}
28
29pub struct SignIn {
32 dir: PathBuf,
33 document: Value,
34 ctx: Context,
35 _one_at_a_time: File,
36}
37
38impl Drop for SignIn {
39 fn drop(&mut self) {
40 let _ = store::discard_signin(&self.ctx, &self.dir);
41 let _ = std::fs::remove_dir_all(&self.dir);
42 }
43}
44
45pub fn sign_in(ctx: &Context) -> Result<SignIn> {
49 let home = home::ensure(ctx).map_err(|source| Error::HomeUnwritable {
50 path: home::dir(ctx),
51 source,
52 })?;
53 let lock_path = home.join("signin.lock");
54 let one_at_a_time = OpenOptions::new()
55 .create(true)
56 .truncate(false)
57 .write(true)
58 .mode(0o600)
59 .open(&lock_path)
60 .map_err(|source| Error::HomeUnwritable {
61 path: lock_path.clone(),
62 source,
63 })?;
64 match one_at_a_time.try_lock() {
65 Ok(()) => {}
66 Err(TryLockError::WouldBlock) => return Err(Error::SignInInProgress),
67 Err(TryLockError::Error(source)) => {
68 return Err(Error::HomeUnwritable {
69 path: lock_path,
70 source,
71 });
72 }
73 }
74
75 let dir = home.join("signin");
76 let _ = std::fs::remove_dir_all(&dir);
77 home::create_private(&dir).map_err(|source| Error::HomeUnwritable {
78 path: dir.clone(),
79 source,
80 })?;
81 let mut pending = SignIn {
82 dir,
83 document: Value::Null,
84 ctx: ctx.clone(),
85 _one_at_a_time: one_at_a_time,
86 };
87
88 let finished = Command::new(&ctx.claude_program)
91 .args(["auth", "login"])
92 .env("CLAUDE_CONFIG_DIR", &pending.dir)
93 .env_remove("CLAUDE_SECURESTORAGE_CONFIG_DIR")
94 .stdout(std::io::stderr())
95 .status()
96 .map_err(|e| match e.kind() {
97 std::io::ErrorKind::NotFound => Error::ClaudeNotFound,
98 _ => Error::SignInIncomplete,
99 })?
100 .success();
101 if !finished {
102 return Err(Error::SignInIncomplete);
103 }
104 let raw = store::read_signin(ctx, &pending.dir)?.ok_or(Error::SignInIncomplete)?;
105 pending.document =
106 serde_json::from_str(&raw).map_err(|e| Error::LiveCredentialShapeUnexpected {
107 detail: e.to_string(),
108 })?;
109 Ok(pending)
110}
111
112pub fn enroll(settled: Settled, label: &str, signed_in: Option<SignIn>) -> Result<Enrolled> {
114 let Settled {
115 _exclusive,
116 mut state,
117 ctx,
118 } = settled;
119 match signed_in {
120 Some(login) => park_signed_in(&ctx, label, &mut state, &login),
121 None => record_current(&ctx, label, &mut state),
122 }
123}
124
125fn claim(state: &State, label: &str, owner: &Owner) -> Result<()> {
127 if let Some(taken) = state.get(label)
128 && taken.account_uuid != owner.account_uuid
129 {
130 return Err(Error::LabelTaken {
131 label: label.to_string(),
132 email: taken.email.clone(),
133 });
134 }
135 if let Some(existing) = state.by_uuid(&owner.account_uuid)
136 && existing.label != label
137 {
138 return Err(Error::AlreadyEnrolled {
139 email: owner.email.clone(),
140 label: existing.label.clone(),
141 });
142 }
143 Ok(())
144}
145
146fn record_current(ctx: &Context, label: &str, state: &mut State) -> Result<Enrolled> {
147 let live = store::read(ctx, &claude::live_service(ctx))?.ok_or(Error::LiveCredentialAbsent)?;
148 let owner = identify(ctx, &access_token(&live)?)?;
149 claim(state, label, &owner)?;
150 let parked = state.get(label).and_then(|a| a.parked.clone());
151 state.upsert(account(label, &owner, parked));
152 state.active = Some(label.to_string());
153 state::save(ctx, state)?;
154 Ok(Enrolled::Current { email: owner.email })
155}
156
157fn park_signed_in(
158 ctx: &Context,
159 label: &str,
160 state: &mut State,
161 login: &SignIn,
162) -> Result<Enrolled> {
163 let owner = identify(ctx, &access_token(&login.document)?)?;
164 claim(state, label, &owner)?;
165 let service = park::reserve(ctx, &owner.account_uuid)?;
166 let fresh = park::store_at(ctx, &service, &oauth_of(&login.document)?)?;
167 let existing = state.get(label);
168 let previous = existing.and_then(|a| a.parked.clone());
169 let renewed = existing.is_some();
170 state.upsert(account(label, &owner, previous));
171 state.park(label, fresh);
172 state::save(ctx, state).inspect_err(|_| {
174 let _ = store::vault_delete(ctx, &service);
175 })?;
176 purge(ctx, state);
177 Ok(if renewed {
178 Enrolled::Renewed { email: owner.email }
179 } else {
180 Enrolled::SignedIn { email: owner.email }
181 })
182}
183
184fn account(label: &str, owner: &Owner, parked: Option<Park>) -> Account {
187 Account {
188 label: label.to_string(),
189 account_uuid: owner.account_uuid.clone(),
190 email: owner.email.clone(),
191 organization_uuid: owner.organization_uuid.clone(),
192 oauth_account: json!({
193 "accountUuid": owner.account_uuid,
194 "emailAddress": owner.email,
195 "organizationUuid": owner.organization_uuid,
196 }),
197 parked,
198 }
199}