Skip to main content

nexo_auth/
error.rs

1use std::path::{Path, PathBuf};
2
3use thiserror::Error;
4
5use crate::handle::{Channel, Fingerprint};
6
7/// Render a credential path for user-facing messages. Paths carrying
8/// the synthetic `inline:` prefix (legacy `agents.<id>.google_auth`
9/// migrated into the store) are replaced with `<inline credential>`
10/// so error output never echoes raw client_id / client_secret values.
11pub fn display_path(p: &Path) -> String {
12    let s = p.to_string_lossy();
13    if s.starts_with("inline:") {
14        "<inline credential>".to_string()
15    } else {
16        s.into_owned()
17    }
18}
19
20#[derive(Debug, Error)]
21pub enum CredentialError {
22    #[error("account '{account}' not found in {channel} store")]
23    NotFound { channel: Channel, account: String },
24
25    #[error("agent '{agent}' not permitted on {channel}:{fp}")]
26    NotPermitted {
27        channel: Channel,
28        agent: String,
29        fp: Fingerprint,
30    },
31
32    #[error(
33        "credential file '{path}' has insecure permissions (mode {mode:o}); run `chmod 600 {path}`",
34        path = display_path(path)
35    )]
36    InsecurePermissions { path: PathBuf, mode: u32 },
37
38    #[error("credential file missing: {path}", path = display_path(path))]
39    FileMissing { path: PathBuf },
40
41    #[error("credential file unreadable ({path}): {source}", path = display_path(path))]
42    Unreadable {
43        path: PathBuf,
44        #[source]
45        source: std::io::Error,
46    },
47
48    #[error("google token expired and no refresh_token; run setup wizard for account '{account}'")]
49    GoogleExpired { account: String },
50
51    #[error("invalid secret file ({path}): {message}", path = display_path(path))]
52    InvalidSecret { path: PathBuf, message: String },
53
54    #[error(
55        "email account '{account}' references google_account_id='{google_account_id}' but no such google account exists"
56    )]
57    OrphanedGoogleRef {
58        account: String,
59        google_account_id: String,
60    },
61}
62
63/// Errors collected by the boot-time gauntlet. The resolver builder
64/// accumulates these in a `Vec` so every misconfiguration surfaces in
65/// one pass rather than one-per-run.
66#[derive(Debug, Error)]
67pub enum BuildError {
68    #[error(
69        "duplicate credential path: '{path}' used by both {a_channel}:{a_instance} and {b_channel}:{b_instance}",
70        path = display_path(path)
71    )]
72    DuplicatePath {
73        path: PathBuf,
74        a_channel: Channel,
75        a_instance: String,
76        b_channel: Channel,
77        b_instance: String,
78    },
79
80    #[error(
81        "overlapping session_dir: '{inner}' is a sub-path of '{outer}' — both would collide on Signal keys",
82        inner = display_path(inner), outer = display_path(outer)
83    )]
84    PathPrefixOverlap { outer: PathBuf, inner: PathBuf },
85
86    #[error("agent '{agent}' binds credentials.{channel}='{account}' but no such {channel} instance exists (available: {available:?})")]
87    MissingInstance {
88        channel: Channel,
89        agent: String,
90        account: String,
91        available: Vec<String>,
92    },
93
94    #[error("agent '{agent}' listens on multiple {channel} instances {instances:?} but did not declare credentials.{channel}; declare it explicitly")]
95    AmbiguousOutbound {
96        channel: Channel,
97        agent: String,
98        instances: Vec<String>,
99    },
100
101    #[error("{channel} instance '{instance}' allow_agents excludes '{agent}' but that agent declares credentials.{channel}='{instance}'")]
102    AllowAgentsExcludes {
103        channel: Channel,
104        instance: String,
105        agent: String,
106    },
107
108    #[error("agent '{agent}': credentials.{channel}='{outbound}' but inbound binding is '{inbound}' — asymmetric; silence with credentials.{channel}_asymmetric: true")]
109    AsymmetricBinding {
110        channel: Channel,
111        agent: String,
112        outbound: String,
113        inbound: String,
114    },
115
116    #[error("{channel} instance '{instance}': {source}")]
117    Credential {
118        channel: Channel,
119        instance: String,
120        #[source]
121        source: CredentialError,
122    },
123
124    #[error("agent '{agent}': inline google_auth is deprecated and not accepted under strict_credentials=true; migrate to config/plugins/google-auth.yaml")]
125    LegacyInlineGoogleAuth { agent: String },
126}
127
128#[derive(Debug, Error)]
129pub enum ResolveError {
130    #[error("agent '{agent}' has no credential bound for channel '{channel}'")]
131    Unbound { agent: String, channel: Channel },
132
133    #[error(transparent)]
134    Credential(#[from] CredentialError),
135}