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