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