1use anyhow::{Context, Result};
2use clap::Parser;
3use colored::Colorize;
4use std::fs;
5use std::io::{self, Write};
6use std::path::PathBuf;
7use std::process::Command as ProcessCommand;
8
9#[derive(Parser, Debug, Clone)]
11#[command(about = "Interactive tutorial for learning Auths concepts")]
12pub struct LearnCommand {
13 #[clap(long, short, value_name = "SECTION")]
15 skip: Option<usize>,
16
17 #[clap(long)]
19 reset: bool,
20
21 #[clap(long)]
23 list: bool,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27enum Section {
28 WhatIsIdentity = 1,
29 CreatingIdentity = 2,
30 SigningCommit = 3,
31 VerifyingSignature = 4,
32 LinkingDevice = 5,
33 RevokingAccess = 6,
34}
35
36impl Section {
37 fn from_number(n: usize) -> Option<Section> {
38 match n {
39 1 => Some(Section::WhatIsIdentity),
40 2 => Some(Section::CreatingIdentity),
41 3 => Some(Section::SigningCommit),
42 4 => Some(Section::VerifyingSignature),
43 5 => Some(Section::LinkingDevice),
44 6 => Some(Section::RevokingAccess),
45 _ => None,
46 }
47 }
48
49 fn title(&self) -> &'static str {
50 match self {
51 Section::WhatIsIdentity => "What is a Cryptographic Identity?",
52 Section::CreatingIdentity => "Creating Your Identity",
53 Section::SigningCommit => "Signing a Commit",
54 Section::VerifyingSignature => "Verifying a Signature",
55 Section::LinkingDevice => "Linking a Second Device",
56 Section::RevokingAccess => "Revoking Access",
57 }
58 }
59
60 fn next(&self) -> Option<Section> {
61 Section::from_number(*self as usize + 1)
62 }
63}
64
65struct Tutorial {
66 sandbox_dir: PathBuf,
67 progress_file: PathBuf,
68}
69
70impl Tutorial {
71 fn new() -> Result<Self> {
72 let home = dirs::home_dir().context("Could not find home directory")?;
73 let sandbox_dir = home.join(".auths-tutorial");
74 let progress_file = sandbox_dir.join(".progress");
75
76 Ok(Self {
77 sandbox_dir,
78 progress_file,
79 })
80 }
81
82 fn setup_sandbox(&self) -> Result<()> {
83 if !self.sandbox_dir.exists() {
84 fs::create_dir_all(&self.sandbox_dir)?;
85 }
86 Ok(())
87 }
88
89 fn cleanup_sandbox(&self) -> Result<()> {
90 if self.sandbox_dir.exists() {
91 fs::remove_dir_all(&self.sandbox_dir)?;
92 }
93 Ok(())
94 }
95
96 fn load_progress(&self) -> usize {
97 if let Ok(content) = fs::read_to_string(&self.progress_file) {
98 content.trim().parse().unwrap_or(1)
99 } else {
100 1
101 }
102 }
103
104 fn save_progress(&self, section: usize) -> Result<()> {
105 fs::write(&self.progress_file, section.to_string())?;
106 Ok(())
107 }
108
109 fn reset_progress(&self) -> Result<()> {
110 if self.progress_file.exists() {
111 fs::remove_file(&self.progress_file)?;
112 }
113 self.cleanup_sandbox()?;
114 Ok(())
115 }
116}
117
118pub fn handle_learn(cmd: LearnCommand) -> Result<()> {
119 let tutorial = Tutorial::new()?;
120
121 if cmd.list {
122 list_sections();
123 return Ok(());
124 }
125
126 if cmd.reset {
127 tutorial.reset_progress()?;
128 println!("{}", "✓ Tutorial progress reset.".green());
129 return Ok(());
130 }
131
132 let start_section = if let Some(skip) = cmd.skip {
133 if !(1..=6).contains(&skip) {
134 anyhow::bail!("Section must be between 1 and 6");
135 }
136 skip
137 } else {
138 tutorial.load_progress()
139 };
140
141 println!();
142 println!(
143 "{}",
144 "╔════════════════════════════════════════════════════════════╗".cyan()
145 );
146 println!(
147 "{}",
148 "║ Welcome to Auths Tutorial ║".cyan()
149 );
150 println!(
151 "{}",
152 "╚════════════════════════════════════════════════════════════╝".cyan()
153 );
154 println!();
155
156 if start_section > 1 {
157 println!(" {} Resuming from section {}", "→".yellow(), start_section);
158 println!();
159 }
160
161 tutorial.setup_sandbox()?;
162
163 let mut current = Section::from_number(start_section).unwrap_or(Section::WhatIsIdentity);
164
165 loop {
166 run_section(current, &tutorial)?;
167 tutorial.save_progress(current as usize + 1)?;
168
169 if let Some(next) = current.next() {
170 println!();
171 print!(
172 " {} Press Enter to continue to the next section (or 'q' to quit): ",
173 "→".yellow()
174 );
175 io::stdout().flush()?;
176
177 let mut input = String::new();
178 io::stdin().read_line(&mut input)?;
179
180 if input.trim().to_lowercase() == "q" {
181 println!();
182 println!(
183 " {} Your progress has been saved. Run 'auths learn' to continue.",
184 "✓".green()
185 );
186 break;
187 }
188
189 current = next;
190 } else {
191 tutorial.cleanup_sandbox()?;
193 tutorial.reset_progress()?;
194
195 println!();
196 println!(
197 "{}",
198 "╔════════════════════════════════════════════════════════════╗".green()
199 );
200 println!(
201 "{}",
202 "║ Tutorial Complete! ║".green()
203 );
204 println!(
205 "{}",
206 "╚════════════════════════════════════════════════════════════╝".green()
207 );
208 println!();
209 println!(" You've learned the basics of Auths! Here's what to do next:");
210 println!();
211 println!(
212 " {} Run {} to create your real identity",
213 "1.".cyan(),
214 "auths init".bold()
215 );
216 println!(
217 " {} Run {} to start signing commits",
218 "2.".cyan(),
219 "auths git setup".bold()
220 );
221 println!(
222 " {} Check {} for advanced features",
223 "3.".cyan(),
224 "auths --help".bold()
225 );
226 println!();
227 break;
228 }
229 }
230
231 Ok(())
232}
233
234fn list_sections() {
235 println!();
236 println!("{}", "Tutorial Sections:".bold());
237 println!();
238
239 for i in 1..=6 {
240 if let Some(section) = Section::from_number(i) {
241 println!(" {} {}", format!("{}.", i).cyan(), section.title());
242 }
243 }
244
245 println!();
246 println!("Use {} to skip to a specific section", "--skip N".bold());
247 println!("Use {} to reset progress", "--reset".bold());
248 println!();
249}
250
251fn run_section(section: Section, tutorial: &Tutorial) -> Result<()> {
252 println!();
253 println!(
254 "{}",
255 "────────────────────────────────────────────────────────────".dimmed()
256 );
257 println!(
258 " {} {}",
259 format!("Section {}", section as usize).cyan().bold(),
260 section.title().bold()
261 );
262 println!(
263 "{}",
264 "────────────────────────────────────────────────────────────".dimmed()
265 );
266 println!();
267
268 match section {
269 Section::WhatIsIdentity => section_what_is_identity()?,
270 Section::CreatingIdentity => section_creating_identity(tutorial)?,
271 Section::SigningCommit => section_signing_commit(tutorial)?,
272 Section::VerifyingSignature => section_verifying_signature(tutorial)?,
273 Section::LinkingDevice => section_linking_device()?,
274 Section::RevokingAccess => section_revoking_access()?,
275 }
276
277 println!();
278 println!(" {} Section complete!", "✓".green());
279
280 Ok(())
281}
282
283fn section_what_is_identity() -> Result<()> {
284 println!(" A cryptographic identity lets you prove who you are without passwords.");
285 println!();
286 println!(" With Auths, your identity consists of:");
287 println!();
288 println!(
289 " {} A unique identifier called a {} (Decentralized Identifier)",
290 "•".cyan(),
291 "DID".bold()
292 );
293 println!(
294 " {} A {} stored in your device's secure storage",
295 "•".cyan(),
296 "signing key".bold()
297 );
298 println!(
299 " {} {} that authorize devices to sign on your behalf",
300 "•".cyan(),
301 "Attestations".bold()
302 );
303 println!();
304 println!(" Key benefits:");
305 println!();
306 println!(
307 " {} {} - No central server owns your identity",
308 "✓".green(),
309 "Decentralized".bold()
310 );
311 println!(
312 " {} {} - Keys never leave your device",
313 "✓".green(),
314 "Secure".bold()
315 );
316 println!(
317 " {} {} - Signatures are mathematically proven",
318 "✓".green(),
319 "Verifiable".bold()
320 );
321 println!(
322 " {} {} - Use the same identity across all your devices",
323 "✓".green(),
324 "Portable".bold()
325 );
326
327 wait_for_continue()?;
328 Ok(())
329}
330
331fn section_creating_identity(tutorial: &Tutorial) -> Result<()> {
332 println!(" Let's create a test identity in a sandbox environment.");
333 println!();
334 println!(" In a real scenario, you would run:");
335 println!();
336 println!(" {}", "$ auths init".cyan());
337 println!();
338 println!(" This creates your identity by:");
339 println!();
340 println!(" {} Generating a cryptographic key pair", "1.".cyan());
341 println!(
342 " {} Storing the private key in your keychain",
343 "2.".cyan()
344 );
345 println!(" {} Creating your DID from the public key", "3.".cyan());
346 println!(
347 " {} Recording everything in a Git repository",
348 "4.".cyan()
349 );
350 println!();
351
352 println!(" {} Creating sandbox identity...", "→".yellow());
354
355 let sandbox_repo = tutorial.sandbox_dir.join("identity");
356 if !sandbox_repo.exists() {
357 fs::create_dir_all(&sandbox_repo)?;
358
359 ProcessCommand::new("git")
361 .args(["init", "--quiet"])
362 .current_dir(&sandbox_repo)
363 .status()?;
364
365 ProcessCommand::new("git")
366 .args(["config", "user.email", "tutorial@auths.io"])
367 .current_dir(&sandbox_repo)
368 .status()?;
369
370 ProcessCommand::new("git")
371 .args(["config", "user.name", "Tutorial User"])
372 .current_dir(&sandbox_repo)
373 .status()?;
374 }
375
376 println!();
377 println!(" {} Sandbox identity created!", "✓".green());
378 println!();
379 println!(" Your sandbox DID would look like:");
380 println!(" {}", "did:keri:EExample123...".dimmed());
381 println!();
382 println!(" This DID is derived from your public key - it's mathematically");
383 println!(" guaranteed to be unique to you.");
384
385 wait_for_continue()?;
386 Ok(())
387}
388
389fn section_signing_commit(tutorial: &Tutorial) -> Result<()> {
390 println!(" Git commit signing proves that commits came from you.");
391 println!();
392 println!(" With Auths configured, Git automatically signs your commits:");
393 println!();
394 println!(" {}", "$ git commit -m \"Add feature\"".cyan());
395 println!(
396 " {}",
397 "[main abc1234] Add feature (auths-signed)".dimmed()
398 );
399 println!();
400 println!(" Behind the scenes, Auths:");
401 println!();
402 println!(" {} Creates a signature of the commit data", "1.".cyan());
403 println!(" {} Uses your key from the secure keychain", "2.".cyan());
404 println!(" {} Embeds the signature in the commit", "3.".cyan());
405 println!();
406
407 let sandbox_repo = tutorial.sandbox_dir.join("identity");
409 let test_file = sandbox_repo.join("test.txt");
410
411 fs::write(&test_file, "Hello from Auths tutorial!\n")?;
412
413 ProcessCommand::new("git")
414 .args(["add", "test.txt"])
415 .current_dir(&sandbox_repo)
416 .status()?;
417
418 ProcessCommand::new("git")
419 .args(["commit", "--quiet", "-m", "Tutorial: First signed commit"])
420 .current_dir(&sandbox_repo)
421 .status()?;
422
423 println!(" {} Created a test commit in the sandbox:", "→".yellow());
424 println!();
425
426 let output = ProcessCommand::new("git")
428 .args(["log", "--oneline", "-1"])
429 .current_dir(&sandbox_repo)
430 .output()?;
431
432 let log_output = String::from_utf8_lossy(&output.stdout);
433 println!(" {}", log_output.trim().dimmed());
434
435 wait_for_continue()?;
436 Ok(())
437}
438
439fn section_verifying_signature(_tutorial: &Tutorial) -> Result<()> {
440 println!(" Anyone can verify that a commit came from you.");
441 println!();
442 println!(" To verify a commit signature:");
443 println!();
444 println!(" {}", "$ auths verify-commit HEAD".cyan());
445 println!();
446 println!(" This checks:");
447 println!();
448 println!(" {} Is the signature mathematically valid?", "•".cyan());
449 println!(
450 " {} Does the signing key match an authorized device?",
451 "•".cyan()
452 );
453 println!(
454 " {} Was the device authorized at commit time?",
455 "•".cyan()
456 );
457 println!();
458 println!(" Verification is fast because it uses local Git data - no network");
459 println!(" calls needed.");
460 println!();
461
462 println!(" {} Example verification output:", "→".yellow());
464 println!();
465 println!(" {}", "✓ Signature valid".green());
466 println!(" {}", " Signer: did:keri:EExample123...".dimmed());
467 println!(" {}", " Device: MacBook Pro (active)".dimmed());
468 println!(" {}", " Signed: 2024-01-15 10:30:00 UTC".dimmed());
469
470 wait_for_continue()?;
471 Ok(())
472}
473
474fn section_linking_device() -> Result<()> {
475 println!(" Use the same identity across multiple devices.");
476 println!();
477 println!(" To link a new device:");
478 println!();
479 println!(" {} On your existing device:", "1.".cyan());
480 println!(" {}", "$ auths pair".cyan());
481 println!(" {}", "Scan this QR code or enter: ABC123".dimmed());
482 println!();
483 println!(" {} On your new device:", "2.".cyan());
484 println!(" {}", "$ auths pair --join ABC123".cyan());
485 println!();
486 println!(
487 " This creates an {} that authorizes the new device",
488 "attestation".bold()
489 );
490 println!(" to sign commits on behalf of your identity.");
491 println!();
492 println!(" {} The new device gets its own signing key", "•".cyan());
493 println!(" {} Your main device signs the authorization", "•".cyan());
494 println!(" {} The attestation is stored in Git", "•".cyan());
495 println!();
496 println!(" You can link phones, tablets, CI servers - any device that");
497 println!(" needs to sign commits as you.");
498
499 wait_for_continue()?;
500 Ok(())
501}
502
503fn section_revoking_access() -> Result<()> {
504 println!(" If a device is lost or compromised, revoke its access.");
505 println!();
506 println!(" To revoke a device:");
507 println!();
508 println!(" {}", "$ auths device revoke <device-did>".cyan());
509 println!();
510 println!(" This creates a revocation record that:");
511 println!();
512 println!(
513 " {} Marks the device as no longer authorized",
514 "•".cyan()
515 );
516 println!(" {} Is signed by your identity", "•".cyan());
517 println!(
518 " {} Is stored in Git and propagates automatically",
519 "•".cyan()
520 );
521 println!();
522 println!(" After revocation, signatures from that device will show as:");
523 println!();
524 println!(" {}", "✗ Device was revoked on 2024-01-20".red());
525 println!();
526 println!(
527 " {} If you suspect compromise, use emergency freeze:",
528 "!".red().bold()
529 );
530 println!();
531 println!(" {}", "$ auths emergency freeze".cyan());
532 println!();
533 println!(" This immediately suspends all signing until you investigate.");
534
535 wait_for_continue()?;
536 Ok(())
537}
538
539fn wait_for_continue() -> Result<()> {
540 println!();
541 print!(" {} Press Enter to continue...", "→".dimmed());
542 io::stdout().flush()?;
543
544 let mut input = String::new();
545 io::stdin().read_line(&mut input)?;
546
547 Ok(())
548}
549
550impl crate::commands::executable::ExecutableCommand for LearnCommand {
551 fn execute(&self, _ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
552 handle_learn(self.clone())
553 }
554}