Skip to main content

auths_cli/commands/
learn.rs

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/// Interactive tutorial for learning Auths concepts.
10#[derive(Parser, Debug, Clone)]
11#[command(about = "Interactive tutorial for learning Auths concepts")]
12pub struct LearnCommand {
13    /// Skip to a specific section (1-6).
14    #[clap(long, short, value_name = "SECTION")]
15    skip: Option<usize>,
16
17    /// Reset progress and start from the beginning.
18    #[clap(long)]
19    reset: bool,
20
21    /// List all tutorial sections.
22    #[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 complete
192            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    // Simulate identity creation
353    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        // Initialize git repo
360        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    // Create a test commit in sandbox
408    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    // Show the commit
427    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    // Show verification output
463    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}