Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: Make node aliases required
Alexis Sellier committed 2 years ago
commit eb701be033610992ed0a1a845e4ab9098dbd53bd
parent 8ed55a19253b9c8943c38dcd922c68c4471e7e0e
53 files changed +898 -614
modified radicle-cli/examples/git/git-pull.md
@@ -1,30 +1,30 @@
```
$ cd heartwood
$ git branch -r
+
  alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
  rad/master
-
  z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```

```
-
$ git ls-remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/heads/*'
+
$ git ls-remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/heads/*'
145e1e69bef3ad93d14946ea212249c2fa9b9828	refs/heads/alice/1
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354	refs/heads/master
```

``` (stderr)
-
$ git fetch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
$ git fetch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
 * [new branch]      alice/1    -> z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
+
 * [new branch]      alice/1    -> alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
```

```
$ git branch -r
+
  alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
+
  alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
  rad/master
-
  z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
-
  z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```

```
-
$ git rev-parse z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
+
$ git rev-parse alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
145e1e69bef3ad93d14946ea212249c2fa9b9828
```
modified radicle-cli/examples/rad-auth.md
@@ -2,7 +2,7 @@ Initializing a new identity with `rad-auth`.
The example below is run with `RAD_PASSPHRASE` set.

```
-
$ rad auth
+
$ rad auth --alias "alice"

Initializing your radicle ๐Ÿ‘พ identity

@@ -18,3 +18,9 @@ You can get the above information at all times using the `self` command:
$ rad self --did
did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
```
+

+
You can also show your alias:
+
```
+
$ rad self --alias
+
alice
+
```
modified radicle-cli/examples/rad-clone-all.md
@@ -7,8 +7,8 @@ $ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope all
โœ“ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSLโ€ฆStBU8Vi..
โœ“ Forking under z6Mkux1โ€ฆnVhib7Z..
โœ“ Creating checkout in ./heartwood..
-
โœ“ Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
-
โœ“ Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSLโ€ฆStBU8Vi
+
โœ“ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
+
โœ“ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSLโ€ฆStBU8Vi
โœ“ Repository successfully cloned under [..]/heartwood/
```

@@ -68,10 +68,10 @@ And fetch his refs:
```
$ git fetch --all
Fetching rad
-
Fetching z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
Fetching alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
Fetching bob
$ git branch --remotes
+
  alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
  bob/master
  rad/master
-
  z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```
modified radicle-cli/examples/rad-clone.md
@@ -7,8 +7,8 @@ $ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope trusted
โœ“ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSLโ€ฆStBU8Vi..
โœ“ Forking under z6Mkt67โ€ฆv4N1tRk..
โœ“ Creating checkout in ./heartwood..
-
โœ“ Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
-
โœ“ Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSLโ€ฆStBU8Vi
+
โœ“ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
+
โœ“ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSLโ€ฆStBU8Vi
โœ“ Repository successfully cloned under [..]/heartwood/
```

@@ -27,8 +27,8 @@ Let's check that the remote tracking branch was setup correctly:

```
$ git branch --remotes
+
  alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
  rad/master
-
  z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```

The first branch is ours, and the second points to the repository delegate.
@@ -36,10 +36,10 @@ We can also take a look at the remotes:

```
$ git remote -v
+
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (fetch)
+
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
rad	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji (fetch)
rad	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (push)
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (fetch)
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
```

Let's check the last commit!
modified radicle-cli/examples/rad-inspect-noauth.md
@@ -2,7 +2,7 @@ The `rad inspect` command can be run without being authenticated with radicle:

``` (fail)
$ rad self
-
โœ— Self failed: Could not load radicle profile
+
โœ— Self failed: Could not load radicle profile: no profile found at path [..]
โœ— Hint: To setup your radicle profile, run `rad auth`.

```
modified radicle-cli/examples/rad-merge-via-push.md
@@ -67,12 +67,12 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
```
```
$ rad patch --merged
-
โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
-
โ”‚ โ—  ID       Title          Author                  Head     +   -   Updated      โ”‚
-
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
-
โ”‚ โœ”  0ec956c  First change   z6MknSLโ€ฆStBU8Vi  (you)  20aa5dd  +0  -0  [   ...    ] โ”‚
-
โ”‚ โœ”  928d76e  Second change  z6MknSLโ€ฆStBU8Vi  (you)  daf349f  +0  -0  [   ...    ] โ”‚
-
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+
โ”‚ โ—  ID       Title          Author                        Head     +   -   Updated      โ”‚
+
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
+
โ”‚ โœ”  0ec956c  First change   z6MknSLโ€ฆStBU8Vi  alice (you)  20aa5dd  +0  -0  [   ...    ] โ”‚
+
โ”‚ โœ”  928d76e  Second change  z6MknSLโ€ฆStBU8Vi  alice (you)  daf349f  +0  -0  [   ...    ] โ”‚
+
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
```

We can verify that the remote tracking branches were also deleted:
modified radicle-cli/examples/rad-remote.md
@@ -48,6 +48,6 @@ able to fetch the node alias from our db!

```
$ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
-
โœ“ Remote bob added
-
โœ“ Remote-tracking branch bob/master created for z6Mkt67โ€ฆv4N1tRk
+
โœ“ Remote bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk added
+
โœ“ Remote-tracking branch bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk/master created for z6Mkt67โ€ฆv4N1tRk
```
modified radicle-cli/examples/rad-self.md
@@ -3,12 +3,14 @@ device and node.

```
$ rad self
+
Alias           alice
DID             did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
โ””โ•ดNode ID (NID) z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
SSH             not running
โ”œโ•ดKey (hash)    SHA256:UIedaL6Cxm6OUErh9GQUzzglSk7VpQlVTI1TAFB/HWA
โ””โ•ดKey (full)    ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHahWSBEpuT1ESZbynOmBNkLBSnR32Ar4woZqSV2YNH1
Home            [..]/home/alice/.radicle
+
โ”œโ•ดConfig        [..]/home/alice/.radicle/config.json
โ”œโ•ดStorage       [..]/home/alice/.radicle/storage
โ”œโ•ดKeys          [..]/home/alice/.radicle/keys
โ””โ•ดNode          [..]/home/alice/.radicle/node
modified radicle-cli/examples/workflow/5-patching-maintainer.md
@@ -112,7 +112,7 @@ $ rad patch show 50e29a1
โ”‚ โ— opened by bob (z6Mkt67โ€ฆv4N1tRk) [   ...    ]                               โ”‚
โ”‚ โ†‘ updated to 3530243d46a2e7a8e4eac7afcbb17cc7c56b3d29 (27857ec) [   ...    ] โ”‚
โ”‚ โ†‘ updated to 744c1f0a75b1c42833c9aa32f79cd40443925d66 (f567f69) [   ...    ] โ”‚
-
โ”‚ โœ“ merged by (you) [   ...    ]                                               โ”‚
+
โ”‚ โœ“ merged by alice (you) [   ...    ]                                         โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
```

modified radicle-cli/examples/workflow/6-pulling-contributor.md
@@ -24,7 +24,7 @@ $ git pull --all --ff
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
   f2de534..f567f69  master     -> rad/master
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
   f2de534..f567f69  master     -> z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
+
   f2de534..f567f69  master     -> alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```

Now our master branch is up to date with the maintainer's master:
@@ -32,5 +32,5 @@ Now our master branch is up to date with the maintainer's master:
```
$ git rev-parse master
f567f695d25b4e8fb63b5f5ad2a584529826e908
-
$ git diff master..z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
+
$ git diff master..alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```
modified radicle-cli/src/commands/auth.rs
@@ -1,10 +1,13 @@
#![allow(clippy::or_fun_call)]
+
use std::env;
use std::ffi::OsString;
+
use std::str::FromStr;

use anyhow::anyhow;

use radicle::crypto::ssh;
use radicle::crypto::ssh::Passphrase;
+
use radicle::node::Alias;
use radicle::profile::env::RAD_PASSPHRASE;
use radicle::{profile, Profile};

@@ -26,6 +29,7 @@ Usage

Options

+
    --alias                 When initializing an identity, sets the node alias
    --stdin                 Read passphrase from stdin (default: false)
    --help                  Print help
"#,
@@ -34,6 +38,7 @@ Options
#[derive(Debug)]
pub struct Options {
    pub stdin: bool,
+
    pub alias: Option<Alias>,
}

impl Args for Options {
@@ -41,10 +46,17 @@ impl Args for Options {
        use lexopt::prelude::*;

        let mut stdin = false;
+
        let mut alias = None;
        let mut parser = lexopt::Parser::from_args(args);

        while let Some(arg) = parser.next()? {
            match arg {
+
                Long("alias") => {
+
                    let val = parser.value()?;
+
                    let val = term::args::alias(&val)?;
+

+
                    alias = Some(val);
+
                }
                Long("stdin") => {
                    stdin = true;
                }
@@ -55,7 +67,7 @@ impl Args for Options {
            }
        }

-
        Ok((Options { stdin }, vec![]))
+
        Ok((Options { alias, stdin }, vec![]))
    }
}

@@ -81,6 +93,16 @@ pub fn init(options: Options) -> anyhow::Result<()> {
        anyhow::bail!("Error retrieving git version; please check your installation");
    }

+
    let alias: Alias = if let Some(alias) = options.alias {
+
        alias
+
    } else {
+
        let user = env::var("USER").ok().and_then(|u| Alias::from_str(&u).ok());
+
        term::input(
+
            "Enter your alias:",
+
            user,
+
            Some("This is your node alias. You can always change it later"),
+
        )?
+
    };
    let home = profile::home()?;
    let passphrase = if options.stdin {
        term::passphrase_stdin()
@@ -88,7 +110,7 @@ pub fn init(options: Options) -> anyhow::Result<()> {
        term::passphrase_confirm("Enter a passphrase:", RAD_PASSPHRASE)
    }?;
    let spinner = term::spinner("Creating your Ed25519 keypair...");
-
    let profile = Profile::init(home, passphrase.clone())?;
+
    let profile = Profile::init(home, alias, passphrase.clone())?;
    spinner.finish();

    match ssh::agent::Agent::connect() {
modified radicle-cli/src/commands/checkout.rs
@@ -160,14 +160,16 @@ pub fn setup_remote(
    remote_name: Option<git::RefString>,
    aliases: &impl AliasStore,
) -> anyhow::Result<()> {
-
    let remote_name = if let Some(alias) = remote_name {
-
        alias
+
    let remote_name = if let Some(name) = remote_name {
+
        name
    } else {
-
        let alias = aliases
-
            .alias(remote_id)
-
            .unwrap_or_else(|| remote_id.to_string());
-
        git::RefString::try_from(alias.clone())
-
            .map_err(|_| anyhow!("invalid remote name: '{alias}'"))?
+
        let name = if let Some(alias) = aliases.alias(remote_id) {
+
            format!("{alias}@{remote_id}")
+
        } else {
+
            remote_id.to_human()
+
        };
+
        git::RefString::try_from(name.as_str())
+
            .map_err(|_| anyhow!("invalid remote name: '{name}'"))?
    };
    let (remote, branch) = setup.run(remote_name, *remote_id)?;

modified radicle-cli/src/commands/init.rs
@@ -198,14 +198,14 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>

    let name = options.name.unwrap_or_else(|| {
        let default = path.file_name().map(|f| f.to_string_lossy().to_string());
-
        term::input("Name", default).unwrap()
+
        term::input("Name", default, None).unwrap()
    });
    let description = options
        .description
-
        .unwrap_or_else(|| term::input("Description", None).unwrap());
+
        .unwrap_or_else(|| term::input("Description", None, None).unwrap());
    let branch = options.branch.unwrap_or_else(|| {
        if interactive.yes() {
-
            term::input("Default branch", Some(head)).unwrap()
+
            term::input("Default branch", Some(head), None).unwrap()
        } else {
            head
        }
modified radicle-cli/src/commands/issue.rs
@@ -462,6 +462,7 @@ fn list(
        ]);
    }
    table.print();
+

    Ok(())
}

modified radicle-cli/src/commands/node/tracking.rs
@@ -53,7 +53,7 @@ fn print_nodes(store: &tracking::store::Config) -> anyhow::Result<()> {
            term::format::highlight(Did::from(id).to_string()),
            match alias {
                None => term::format::secondary(String::from("n/a")),
-
                Some(alias) => term::format::secondary(alias),
+
                Some(alias) => term::format::secondary(alias.to_string()),
            },
            term::format::secondary(policy.to_string()),
        ]);
modified radicle-cli/src/commands/patch/list.rs
@@ -93,7 +93,7 @@ pub fn run(
/// Patch row.
pub fn row(
    profile: &Profile,
-
    alias: Option<String>,
+
    alias: Option<Alias>,
    id: &PatchId,
    patch: &Patch,
    repository: &Repository,
modified radicle-cli/src/commands/self.rs
@@ -18,6 +18,7 @@ Usage

Options

+
    --alias              Show your Node alias
    --nid                Show your Node ID (NID)
    --did                Show your DID
    --home               Show your Radicle home
@@ -29,6 +30,7 @@ Options

#[derive(Debug)]
enum Show {
+
    Alias,
    NodeId,
    Did,
    Home,
@@ -51,6 +53,9 @@ impl Args for Options {

        while let Some(arg) = parser.next()? {
            match arg {
+
                Long("alias") if show.is_none() => {
+
                    show = Some(Show::Alias);
+
                }
                Long("nid") if show.is_none() => {
                    show = Some(Show::NodeId);
                }
@@ -86,6 +91,9 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
    let profile = ctx.profile()?;

    match options.show {
+
        Show::Alias => {
+
            term::print(profile.config.alias());
+
        }
        Show::NodeId => {
            term::print(profile.id());
        }
@@ -110,6 +118,11 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
fn all(profile: &Profile) -> anyhow::Result<()> {
    let mut table = term::Table::default();

+
    table.push([
+
        term::format::style("Alias").to_string(),
+
        term::format::primary(profile.config.alias()).to_string(),
+
    ]);
+

    let did = profile.did();
    table.push([
        term::format::style("DID").to_string(),
@@ -153,6 +166,12 @@ fn all(profile: &Profile) -> anyhow::Result<()> {
        term::format::tertiary(home.display()).to_string(),
    ]);

+
    let config_path = profile.home.config();
+
    table.push([
+
        term::format::style("โ”œโ•ดConfig").to_string(),
+
        term::format::tertiary(config_path.display()).to_string(),
+
    ]);
+

    let storage_path = profile.home.storage();
    table.push([
        term::format::style("โ”œโ•ดStorage").to_string(),
modified radicle-cli/src/commands/track.rs
@@ -79,10 +79,7 @@ impl Args for Options {
                }
                (Long("alias"), Some(Operation::TrackNode { alias, .. })) => {
                    let name = parser.value()?;
-
                    let name = name
-
                        .to_str()
-
                        .to_owned()
-
                        .ok_or_else(|| anyhow!("alias specified is not UTF-8"))?;
+
                    let name = term::args::alias(&name)?;

                    *alias = Some(name.to_owned());
                }
modified radicle-cli/src/terminal.rs
@@ -113,14 +113,13 @@ where

/// Get the default profile. Fails if there is no profile.
pub fn profile() -> Result<Profile, anyhow::Error> {
-
    let error = args::Error::WithHint {
-
        err: anyhow::anyhow!("Could not load radicle profile"),
-
        hint: "To setup your radicle profile, run `rad auth`.",
-
    };
-

    match Profile::load() {
        Ok(profile) => Ok(profile),
-
        Err(_) => Err(error.into()),
+
        Err(e) => Err(args::Error::WithHint {
+
            err: anyhow::anyhow!("Could not load radicle profile: {e}"),
+
            hint: "To setup your radicle profile, run `rad auth`.",
+
        }
+
        .into()),
    }
}

modified radicle-cli/src/terminal/args.rs
@@ -6,7 +6,7 @@ use anyhow::anyhow;
use radicle::cob::{self, issue, patch};
use radicle::crypto;
use radicle::git::RefString;
-
use radicle::node::Address;
+
use radicle::node::{Address, Alias};
use radicle::prelude::{Did, Id, NodeId};

#[derive(thiserror::Error, Debug)]
@@ -136,6 +136,15 @@ pub fn string(val: &OsString) -> String {
    val.to_string_lossy().to_string()
}

+
pub fn alias(val: &OsString) -> anyhow::Result<Alias> {
+
    let val = val.as_os_str();
+
    let val = val
+
        .to_str()
+
        .ok_or_else(|| anyhow!("alias must be valid UTF-8"))?;
+

+
    Alias::from_str(val).map_err(|e| e.into())
+
}
+

pub fn issue(val: &OsString) -> anyhow::Result<issue::IssueId> {
    let val = val.to_string_lossy();
    issue::IssueId::from_str(&val).map_err(|_| anyhow!("invalid Issue ID '{}'", val))
modified radicle-cli/src/terminal/format.rs
@@ -4,7 +4,7 @@ pub use radicle_term::format::*;
pub use radicle_term::{style, Paint};

use radicle::cob::{ObjectId, Timestamp};
-
use radicle::node::{AliasStore, NodeId};
+
use radicle::node::{Alias, AliasStore, NodeId};
use radicle::prelude::Did;
use radicle::profile::Profile;
use radicle_term::element::Line;
@@ -143,15 +143,15 @@ impl<'a> fmt::Display for Identity<'a> {
pub enum Author<'a> {
    Author {
        nid: &'a NodeId,
-
        alias: Option<String>,
+
        alias: Option<Alias>,
    },
    Me {
-
        alias: Option<String>,
+
        alias: Option<Alias>,
    },
}

impl<'a> Author<'a> {
-
    pub fn new(nid: &'a NodeId, alias: Option<String>, me: &Profile) -> Author<'a> {
+
    pub fn new(nid: &'a NodeId, alias: Option<Alias>, me: &Profile) -> Author<'a> {
        if nid == me.id() {
            Self::Me { alias }
        } else {
modified radicle-cli/tests/commands.rs
@@ -3,6 +3,7 @@ use std::str::FromStr;
use std::{env, fs, thread, time};

use radicle::git;
+
use radicle::node::Alias;
use radicle::node::Handle as _;
use radicle::prelude::Id;
use radicle::profile::Home;
@@ -48,6 +49,7 @@ fn test<'a>(
        .env("EDITOR", "true")
        .env("TZ", "UTC")
        .env("LANG", "C")
+
        .env("USER", "alice")
        .env(radicle_cob::git::RAD_COMMIT_TIME, "1671125284")
        .envs(git::env::GIT_DEFAULT_CONFIG)
        .envs(envs)
@@ -234,12 +236,12 @@ fn rad_node() {
    logger::init(log::Level::Debug);

    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
+
    let alice = environment.node(Config::new(Alias::new("alice")));
+
    let bob = environment.node(Config::new(Alias::new("bob")));
    let working = tempfile::tempdir().unwrap();

-
    let alice = alice.spawn(Config::default());
-
    let _bob = bob.spawn(Config::default());
+
    let alice = alice.spawn();
+
    let _bob = bob.spawn();

    fixtures::repository(working.path().join("alice"));

@@ -398,9 +400,9 @@ fn rad_rm() {
#[test]
fn rad_track() {
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
+
    let alice = environment.node(Config::new(Alias::new("alice")));
    let working = tempfile::tempdir().unwrap();
-
    let alice = alice.spawn(Config::default());
+
    let alice = alice.spawn();

    test(
        "examples/rad-track.md",
@@ -416,15 +418,15 @@ fn rad_clone() {
    logger::init(log::Level::Debug);

    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
+
    let mut alice = environment.node(Config::new(Alias::new("alice")));
+
    let bob = environment.node(Config::new(Alias::new("bob")));
    let working = environment.tmp().join("working");

    // Setup a test project.
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");

-
    let mut alice = alice.spawn(Config::default());
-
    let mut bob = bob.spawn(Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
    // Prevent Alice from fetching Bob's fork, as we're not testing that and it may cause errors.
    alice.handle.track_repo(acme, Scope::Trusted).unwrap();

@@ -438,17 +440,17 @@ fn rad_clone_all() {
    logger::init(log::Level::Debug);

    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let eve = environment.node("eve");
+
    let mut alice = environment.node(Config::new(Alias::new("alice")));
+
    let bob = environment.node(Config::new(Alias::new("bob")));
+
    let eve = environment.node(Config::new(Alias::new("eve")));
    let working = environment.tmp().join("working");

    // Setup a test project.
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");

-
    let mut alice = alice.spawn(Config::default());
-
    let mut bob = bob.spawn(Config::default());
-
    let mut eve = eve.spawn(Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let mut eve = eve.spawn();

    alice.handle.track_repo(acme, Scope::All).unwrap();
    bob.connect(&alice).converge([&alice]);
@@ -477,7 +479,7 @@ fn rad_clone_all() {
#[test]
fn rad_self() {
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
+
    let alice = environment.node(Config::new(Alias::new("alice")));
    let working = environment.tmp().join("working");

    test("examples/rad-self.md", working, Some(&alice.home), []).unwrap();
@@ -488,10 +490,10 @@ fn rad_clone_unknown() {
    logger::init(log::Level::Debug);

    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
+
    let alice = environment.node(Config::new(Alias::new("alice")));
    let working = environment.tmp().join("working");

-
    let alice = alice.spawn(Config::default());
+
    let alice = alice.spawn();

    test(
        "examples/rad-clone-unknown.md",
@@ -507,12 +509,12 @@ fn rad_init_sync_and_clone() {
    logger::init(log::Level::Debug);

    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
+
    let alice = environment.node(Config::new(Alias::new("alice")));
+
    let bob = environment.node(Config::new(Alias::new("bob")));
    let working = environment.tmp().join("working");

-
    let alice = alice.spawn(Config::default());
-
    let mut bob = bob.spawn(Config::default());
+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();

    bob.connect(&alice);

@@ -548,11 +550,11 @@ fn rad_init_sync_and_clone() {
fn rad_fetch() {
    let mut environment = Environment::new();
    let working = environment.tmp().join("working");
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
+
    let alice = environment.node(Config::new(Alias::new("alice")));
+
    let bob = environment.node(Config::new(Alias::new("bob")));

-
    let mut alice = alice.spawn(Config::default());
-
    let bob = bob.spawn(Config::default());
+
    let mut alice = alice.spawn();
+
    let bob = bob.spawn();

    alice.connect(&bob);
    fixtures::repository(working.join("alice"));
@@ -582,11 +584,11 @@ fn rad_fetch() {
fn rad_fork() {
    let mut environment = Environment::new();
    let working = environment.tmp().join("working");
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
+
    let alice = environment.node(Config::new(Alias::new("alice")));
+
    let bob = environment.node(Config::new(Alias::new("bob")));

-
    let mut alice = alice.spawn(Config::default());
-
    let bob = bob.spawn(Config::default());
+
    let mut alice = alice.spawn();
+
    let bob = bob.spawn();

    alice.connect(&bob);
    fixtures::repository(working.join("alice"));
@@ -625,10 +627,10 @@ fn test_clone_without_seeds() {
    logger::init(log::Level::Debug);

    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
+
    let mut alice = environment.node(Config::new(Alias::new("alice")));
    let working = environment.tmp().join("working");
    let rid = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
-
    let mut alice = alice.spawn(Config::default());
+
    let mut alice = alice.spawn();
    let seeds = alice.handle.seeds(rid).unwrap();

    assert!(!seeds.has_connections());
@@ -648,13 +650,13 @@ fn test_cob_replication() {

    let mut environment = Environment::new();
    let working = tempfile::tempdir().unwrap();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
+
    let mut alice = environment.node(Config::new(Alias::new("alice")));
+
    let bob = environment.node(Config::new(Alias::new("bob")));

    let rid = alice.project("heartwood", "");

-
    let mut alice = alice.spawn(Config::default());
-
    let mut bob = bob.spawn(Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
    let events = alice.handle.events();

    alice.handle.track_node(bob.id, None).unwrap();
@@ -708,13 +710,13 @@ fn test_cob_replication() {
fn test_cob_deletion() {
    let mut environment = Environment::new();
    let working = tempfile::tempdir().unwrap();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
+
    let mut alice = environment.node(Config::new(Alias::new("alice")));
+
    let bob = environment.node(Config::new(Alias::new("bob")));

    let rid = alice.project("heartwood", "");

-
    let mut alice = alice.spawn(Config::default());
-
    let mut bob = bob.spawn(Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();

    alice.handle.track_repo(rid, Scope::All).unwrap();
    bob.handle.track_repo(rid, Scope::All).unwrap();
@@ -762,9 +764,9 @@ fn rad_sync() {

    let mut environment = Environment::new();
    let working = environment.tmp().join("working");
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let eve = environment.node("eve");
+
    let alice = environment.node(Config::new(Alias::new("alice")));
+
    let bob = environment.node(Config::new(Alias::new("bob")));
+
    let eve = environment.node(Config::new(Alias::new("eve")));
    let acme = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();

    fixtures::repository(working.join("acme"));
@@ -777,9 +779,9 @@ fn rad_sync() {
    )
    .unwrap();

-
    let mut alice = alice.spawn(Config::default());
-
    let mut bob = bob.spawn(Config::default());
-
    let mut eve = eve.spawn(Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let mut eve = eve.spawn();

    bob.handle.track_repo(acme, Scope::All).unwrap();
    eve.handle.track_repo(acme, Scope::All).unwrap();
@@ -806,19 +808,19 @@ fn rad_sync() {
//
fn test_replication_via_seed() {
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let seed = environment.node("seed");
-
    let working = environment.tmp().join("working");
-
    let rid = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    let mut alice = alice.spawn(Config::default());
-
    let mut bob = bob.spawn(Config::default());
-
    let seed = seed.spawn(Config {
+
    let alice = environment.node(Config::new(Alias::new("alice")));
+
    let bob = environment.node(Config::new(Alias::new("bob")));
+
    let seed = environment.node(Config {
        policy: Policy::Track,
        scope: Scope::All,
-
        ..Config::default()
+
        ..Config::new(Alias::new("seed"))
    });
+
    let working = environment.tmp().join("working");
+
    let rid = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let seed = seed.spawn();

    alice.connect(&seed);
    bob.connect(&seed);
@@ -891,8 +893,8 @@ fn test_replication_via_seed() {
#[test]
fn rad_remote() {
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
+
    let alice = environment.node(Config::new(Alias::new("alice")));
+
    let bob = environment.node(Config::new(Alias::new("bob")));
    let working = environment.tmp().join("working");
    let home = alice.home.clone();
    let rid = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
@@ -907,11 +909,11 @@ fn rad_remote() {
    )
    .unwrap();

-
    let mut alice = alice.spawn(Config::default());
-
    let mut bob = bob.spawn(Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
    alice
        .handle
-
        .track_node(bob.id, Some("bob".to_owned()))
+
        .track_node(bob.id, Some(Alias::new("bob")))
        .unwrap();

    bob.connect(&alice);
@@ -935,7 +937,7 @@ fn rad_merge_via_push() {
    logger::init(log::Level::Debug);

    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
+
    let alice = environment.node(Config::new(Alias::new("alice")));
    let working = environment.tmp().join("working");

    fixtures::repository(working.join("alice"));
@@ -948,7 +950,7 @@ fn rad_merge_via_push() {
    )
    .unwrap();

-
    let alice = alice.spawn(Config::default());
+
    let alice = alice.spawn();

    test(
        "examples/rad-merge-via-push.md",
@@ -964,8 +966,8 @@ fn git_push_and_pull() {
    logger::init(log::Level::Debug);

    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
+
    let alice = environment.node(Config::new(Alias::new("alice")));
+
    let bob = environment.node(Config::new(Alias::new("bob")));
    let working = environment.tmp().join("working");

    fixtures::repository(working.join("alice"));
@@ -978,8 +980,8 @@ fn git_push_and_pull() {
    )
    .unwrap();

-
    let alice = alice.spawn(Config::default());
-
    let mut bob = bob.spawn(Config::default());
+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();

    bob.connect(&alice).converge([&alice]);

@@ -1016,8 +1018,8 @@ fn git_push_and_pull() {
#[test]
fn rad_workflow() {
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
+
    let alice = environment.node(Config::new(Alias::new("alice")));
+
    let bob = environment.node(Config::new(Alias::new("bob")));
    let working = environment.tmp().join("working");

    fixtures::repository(working.join("alice"));
@@ -1030,11 +1032,8 @@ fn rad_workflow() {
    )
    .unwrap();

-
    let alice = alice.spawn(Config::default());
-
    let mut bob = bob.spawn(Config {
-
        alias: Some("bob".to_string()),
-
        ..Default::default()
-
    });
+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();

    bob.connect(&alice).converge([&alice]);

modified radicle-httpd/src/api/json.rs
@@ -14,7 +14,7 @@ use radicle::cob::thread;
use radicle::cob::thread::CommentId;
use radicle::cob::{ActorId, Author, Reaction, Timestamp};
use radicle::git::RefString;
-
use radicle::node::AliasStore;
+
use radicle::node::{Alias, AliasStore};
use radicle::prelude::NodeId;
use radicle::storage::{git, refs, ReadRepository};
use radicle_surf::blob::Blob;
@@ -146,7 +146,7 @@ pub(crate) fn patch(
}

/// Returns JSON for an `author` and fills in `alias` when present.
-
fn author(author: &Author, alias: Option<String>) -> Value {
+
fn author(author: &Author, alias: Option<Alias>) -> Value {
    match alias {
        Some(alias) => json!({
            "id": author.id,
@@ -157,7 +157,7 @@ fn author(author: &Author, alias: Option<String>) -> Value {
}

/// Returns JSON for a patch `Merge` and fills in `alias` when present.
-
fn merge(merge: &Merge, nid: &NodeId, alias: Option<String>) -> Value {
+
fn merge(merge: &Merge, nid: &NodeId, alias: Option<Alias>) -> Value {
    match alias {
        Some(alias) => json!({
            "author": {
@@ -180,7 +180,7 @@ fn merge(merge: &Merge, nid: &NodeId, alias: Option<String>) -> Value {
}

/// Returns JSON for a patch `Review` and fills in `alias` when present.
-
fn review(nid: &NodeId, alias: Option<String>, review: &Review) -> Value {
+
fn review(nid: &NodeId, alias: Option<Alias>, review: &Review) -> Value {
    match alias {
        Some(alias) => json!({
            "author": {
modified radicle-httpd/src/test.rs
@@ -16,9 +16,11 @@ use radicle::crypto::ssh::keystore::MemorySigner;
use radicle::crypto::ssh::Keystore;
use radicle::crypto::{KeyPair, Seed, Signer};
use radicle::git::{raw as git2, RefString};
+
use radicle::node;
use radicle::node::address as AddressStore;
use radicle::node::routing as RoutingStore;
use radicle::node::tracking::store as TrackingStore;
+
use radicle::profile;
use radicle::profile::Home;
use radicle::storage::ReadStorage;
use radicle::Storage;
@@ -63,6 +65,9 @@ pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile {
        storage,
        keystore,
        public_key: keypair.pk.into(),
+
        config: profile::Config {
+
            node: node::Config::new(node::Alias::new("seed")),
+
        },
    }
}

modified radicle-node/src/control.rs
@@ -13,7 +13,7 @@ use serde_json as json;

use crate::identity::Id;
use crate::node::NodeId;
-
use crate::node::{Command, CommandName, CommandResult};
+
use crate::node::{Alias, Command, CommandName, CommandResult};
use crate::runtime;
use crate::runtime::thread;

@@ -150,6 +150,12 @@ where
                [node, alias] => (node.as_str(), Some(alias.to_owned())),
                _ => return Err(CommandError::InvalidCommandArgs(cmd.args)),
            };
+
            let alias = alias
+
                .map(|a| {
+
                    Alias::from_str(&a)
+
                        .map_err(|e| CommandError::InvalidCommandArg(node.to_owned(), Box::new(e)))
+
                })
+
                .transpose()?;
            let nid = node
                .parse()
                .map_err(|e| CommandError::InvalidCommandArg(node.to_owned(), Box::new(e)))?;
@@ -358,12 +364,8 @@ mod tests {
        assert!(handle.untrack_repo(proj).unwrap());
        assert!(!handle.untrack_repo(proj).unwrap());

-
        assert!(handle
-
            .track_node(peer, Some(String::from("alice")))
-
            .unwrap());
-
        assert!(!handle
-
            .track_node(peer, Some(String::from("alice")))
-
            .unwrap());
+
        assert!(handle.track_node(peer, Some(Alias::new("alice"))).unwrap());
+
        assert!(!handle.track_node(peer, Some(Alias::new("alice"))).unwrap());
        assert!(handle.untrack_node(peer).unwrap());
        assert!(!handle.untrack_node(peer).unwrap());
    }
modified radicle-node/src/main.rs
@@ -5,11 +5,11 @@ use crossbeam_channel as chan;
use cyphernet::addr::PeerAddr;
use localtime::LocalDuration;

+
use radicle::node;
use radicle::prelude::Signer;
use radicle::profile;
use radicle_node::crypto::ssh::keystore::{Keystore, MemorySigner};
use radicle_node::prelude::{Address, NodeId};
-
use radicle_node::service::tracking::{Policy, Scope};
use radicle_node::Runtime;
use radicle_node::{logger, service, signals};
use radicle_term as term;
@@ -21,7 +21,6 @@ Usage

Options

-
    --alias              <alias>        Identify yourself with an alias on the network
    --connect            <peer>         Connect to the given peer address on start
    --external-address   <address>      Publicly accessible address (default 0.0.0.0:8776)
    --git-daemon         <address>      Address to bind git-daemon to (default 0.0.0.0:9418)
@@ -34,48 +33,29 @@ Options

#[derive(Debug)]
struct Options {
-
    alias: Option<String>,
-
    connect: Vec<(NodeId, Address)>,
-
    external_addresses: Vec<Address>,
    daemon: Option<net::SocketAddr>,
-
    limits: service::config::Limits,
    listen: Vec<net::SocketAddr>,
    force: bool,
-
    tracking_policy: Policy,
-
    tracking_scope: Scope,
}

impl Options {
-
    fn from_env() -> Result<Self, anyhow::Error> {
+
    fn from_env(config: &mut node::Config) -> Result<Self, anyhow::Error> {
        use lexopt::prelude::*;

        let mut parser = lexopt::Parser::from_env();
-
        let mut alias = None;
-
        let mut connect = Vec::new();
-
        let mut external_addresses = Vec::new();
-
        let mut limits = service::config::Limits::default();
        let mut listen = Vec::new();
        let mut daemon = None;
-
        let mut tracking_policy = Policy::default();
-
        let mut tracking_scope = Scope::default();
        let mut force = false;

        while let Some(arg) = parser.next()? {
            match arg {
-
                Long("alias") => {
-
                    let name: String = parser.value()?.parse()?;
-
                    if name.len() > 32 {
-
                        anyhow::bail!("alias '{}' is longer than 32 characters", name);
-
                    }
-
                    alias = Some(name);
-
                }
                Long("connect") => {
                    let peer: PeerAddr<NodeId, Address> = parser.value()?.parse()?;
-
                    connect.push((peer.id, peer.addr.clone()));
+
                    config.connect.push(peer.into());
                }
                Long("external-address") => {
                    let addr = parser.value()?.parse()?;
-
                    external_addresses.push(addr);
+
                    config.external_addresses.push(addr);
                }
                Long("force") => {
                    force = true;
@@ -89,24 +69,24 @@ impl Options {
                        .value()?
                        .parse()
                        .map_err(|s| anyhow!("unknown tracking policy {:?}", s))?;
-
                    tracking_policy = policy;
+
                    config.policy = policy;
                }
                Long("tracking-scope") => {
                    let scope = parser
                        .value()?
                        .parse()
                        .map_err(|s| anyhow!("unknown tracking scope {:?}", s))?;
-
                    tracking_scope = scope;
+
                    config.scope = scope;
                }
                Long("limit-routing-max-age") => {
                    let secs: u64 = parser.value()?.parse()?;
-
                    limits.routing_max_age = LocalDuration::from_secs(secs);
+
                    config.limits.routing_max_age = LocalDuration::from_secs(secs);
                }
                Long("limit-routing-max-size") => {
-
                    limits.routing_max_size = parser.value()?.parse()?;
+
                    config.limits.routing_max_size = parser.value()?.parse()?;
                }
                Long("limit-fetch-concurrency") => {
-
                    limits.fetch_concurrency = parser.value()?.parse()?;
+
                    config.limits.fetch_concurrency = parser.value()?.parse()?;
                }
                Long("listen") => {
                    let addr = parser.value()?.parse()?;
@@ -120,7 +100,7 @@ impl Options {
            }
        }

-
        if external_addresses.len() > service::ADDRESS_LIMIT {
+
        if config.external_addresses.len() > service::ADDRESS_LIMIT {
            anyhow::bail!(
                "external address limit ({}) exceeded",
                service::ADDRESS_LIMIT,
@@ -128,15 +108,9 @@ impl Options {
        }

        Ok(Self {
-
            alias,
-
            connect,
            daemon,
-
            external_addresses,
            force,
-
            limits,
            listen,
-
            tracking_policy,
-
            tracking_scope,
        })
    }
}
@@ -144,13 +118,14 @@ impl Options {
fn execute() -> anyhow::Result<()> {
    logger::init(log::Level::Debug)?;

-
    let options = Options::from_env()?;
-

    log::info!(target: "node", "Starting node..");
    log::info!(target: "node", "Version {} ({})", env!("CARGO_PKG_VERSION"), env!("GIT_HEAD"));
-
    log::info!(target: "node", "Unlocking node keystore..");

    let home = profile::home()?;
+
    let mut config = profile::Config::load(&home.config())?.node;
+

+
    log::info!(target: "node", "Unlocking node keystore..");
+

    let passphrase = term::io::passphrase(profile::env::RAD_PASSPHRASE)
        .context(format!("`{}` must be set", profile::env::RAD_PASSPHRASE))?;
    let keystore = Keystore::new(&home.keys());
@@ -158,15 +133,7 @@ fn execute() -> anyhow::Result<()> {

    log::info!(target: "node", "Node ID is {}", signer.public_key());

-
    let config = service::Config {
-
        alias: options.alias,
-
        connect: options.connect.into_iter().collect(),
-
        external_addresses: options.external_addresses,
-
        limits: options.limits,
-
        policy: options.tracking_policy,
-
        scope: options.tracking_scope,
-
        ..service::Config::default()
-
    };
+
    let options = Options::from_env(&mut config)?;
    let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050);
    let daemon = options.daemon.unwrap_or_else(|| {
        net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), radicle::git::PROTOCOL_PORT)
modified radicle-node/src/runtime.rs
@@ -154,7 +154,10 @@ impl Runtime {
            .ok()
            .and_then(|ann| NodeAnnouncement::decode(&mut ann.as_slice()).ok())
            .and_then(|ann| {
-
                if config.matches(&ann) {
+
                if config.features() == ann.features
+
                    && config.alias == ann.alias
+
                    && config.external_addresses == ann.addresses.as_ref()
+
                {
                    Some(ann)
                } else {
                    None
@@ -168,8 +171,7 @@ impl Runtime {
            );
            ann
        } else {
-
            config
-
                .node(clock.as_secs())
+
            service::gossip::node(&config, clock.as_secs())
                .solve(Default::default())
                .expect("Runtime::init: unable to solve proof-of-work puzzle")
        };
modified radicle-node/src/runtime/handle.rs
@@ -9,7 +9,7 @@ use reactor::poller::popol::PopolWaker;
use thiserror::Error;

use crate::identity::Id;
-
use crate::node::{Command, FetchResult};
+
use crate::node::{Alias, Command, FetchResult};
use crate::profile::Home;
use crate::runtime::Emitter;
use crate::service;
@@ -161,7 +161,7 @@ impl radicle::node::Handle for Handle {
        receiver.recv().map_err(Error::from)
    }

-
    fn track_node(&mut self, id: NodeId, alias: Option<String>) -> Result<bool, Error> {
+
    fn track_node(&mut self, id: NodeId, alias: Option<Alias>) -> Result<bool, Error> {
        let (sender, receiver) = chan::bounded(1);
        self.command(service::Command::TrackNode(id, alias, sender))?;
        receiver.recv().map_err(Error::from)
modified radicle-node/src/service.rs
@@ -1,7 +1,6 @@
#![allow(clippy::too_many_arguments)]
#![allow(clippy::collapsible_match)]
#![allow(clippy::collapsible_if)]
-
pub mod config;
pub mod filter;
pub mod io;
pub mod message;
@@ -28,7 +27,7 @@ use crate::identity::IdentityError;
use crate::identity::{Doc, Id};
use crate::node::routing;
use crate::node::routing::InsertResult;
-
use crate::node::{Address, Features, FetchResult, Seed, Seeds};
+
use crate::node::{Address, Alias, Features, FetchResult, Seed, Seeds};
use crate::prelude::*;
use crate::runtime::Emitter;
use crate::service::message::{Announcement, AnnouncementMessage, Ping};
@@ -41,8 +40,7 @@ use crate::worker::FetchError;
use crate::Link;

pub use crate::node::events::{Event, Events};
-
pub use crate::node::NodeId;
-
pub use crate::service::config::{Config, Network};
+
pub use crate::node::{config::Network, Config, NodeId};
pub use crate::service::message::{Message, ZeroBytes};
pub use crate::service::session::Session;

@@ -137,7 +135,7 @@ pub enum Command {
    /// Untrack the given repository.
    UntrackRepo(Id, chan::Sender<bool>),
    /// Track the given node.
-
    TrackNode(NodeId, Option<String>, chan::Sender<bool>),
+
    TrackNode(NodeId, Option<Alias>, chan::Sender<bool>),
    /// Untrack the given node.
    UntrackNode(NodeId, chan::Sender<bool>),
    /// Query the internal service state.
@@ -382,7 +380,7 @@ where

        // Connect to configured peers.
        let addrs = self.config.connect.clone();
-
        for (id, addr) in addrs {
+
        for (id, addr) in addrs.into_iter().map(|ca| ca.into()) {
            self.connect(id, addr);
        }
        // Ensure that our inventory is recorded in our routing table, and we are tracking
@@ -405,7 +403,7 @@ where
            .insert(
                &self.node_id(),
                self.node.features,
-
                self.node.alias().unwrap_or_default(),
+
                self.node.alias.clone(),
                self.node.work(),
                self.node.timestamp,
                self.node
@@ -998,14 +996,6 @@ where
                    return Ok(false);
                }

-
                let alias = match ann.alias() {
-
                    Ok(s) => s,
-
                    Err(e) => {
-
                        warn!(target: "service", "Dropping node announcement from {announcer}: invalid alias: {e}");
-
                        return Ok(false);
-
                    }
-
                };
-

                // If this node isn't a seed, we're not interested in adding it
                // to our address book, but other nodes may be, so we relay the message anyway.
                if !features.has(Features::SEED) {
@@ -1015,7 +1005,7 @@ where
                match self.addresses.insert(
                    announcer,
                    *features,
-
                    alias,
+
                    ann.alias.clone(),
                    ann.work(),
                    timestamp,
                    addresses
@@ -1781,7 +1771,7 @@ impl DerefMut for Sessions {
    }
}

-
mod gossip {
+
pub mod gossip {
    use super::*;
    use crate::service::filter::Filter;

@@ -1842,6 +1832,24 @@ mod gossip {
        ]
    }

+
    pub fn node(config: &Config, timestamp: Timestamp) -> NodeAnnouncement {
+
        let features = config.features();
+
        let alias = config.alias.clone();
+
        let addresses: BoundedVec<_, ADDRESS_LIMIT> = config
+
            .external_addresses
+
            .clone()
+
            .try_into()
+
            .expect("external addresses are within the limit");
+

+
        NodeAnnouncement {
+
            features,
+
            timestamp,
+
            alias,
+
            addresses,
+
            nonce: 0,
+
        }
+
    }
+

    pub fn inventory(timestamp: Timestamp, inventory: Vec<Id>) -> InventoryAnnouncement {
        type Inventory = BoundedVec<Id, INVENTORY_LIMIT>;

deleted radicle-node/src/service/config.rs
@@ -1,147 +0,0 @@
-
use localtime::LocalDuration;
-

-
use radicle::node;
-
use radicle::node::Address;
-

-
use crate::bounded::BoundedVec;
-
use crate::service::message::{NodeAnnouncement, ADDRESS_LIMIT};
-
use crate::service::tracking::{Policy, Scope};
-
use crate::service::NodeId;
-

-
/// Peer-to-peer network.
-
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
-
pub enum Network {
-
    #[default]
-
    Main,
-
    Test,
-
}
-

-
/// Configuration parameters defining attributes of minima and maxima.
-
#[derive(Debug, Clone)]
-
pub struct Limits {
-
    /// Number of routing table entries before we start pruning.
-
    pub routing_max_size: usize,
-
    /// How long to keep a routing table entry before being pruned.
-
    pub routing_max_age: LocalDuration,
-
    /// Maximum number of concurrent fetches per per connection.
-
    pub fetch_concurrency: usize,
-
}
-

-
impl Default for Limits {
-
    fn default() -> Self {
-
        Self {
-
            routing_max_size: 1000,
-
            routing_max_age: LocalDuration::from_mins(7 * 24 * 60),
-
            fetch_concurrency: 1,
-
        }
-
    }
-
}
-

-
/// Service configuration.
-
#[derive(Debug, Clone)]
-
pub struct Config {
-
    /// Alias chosen by the operator.
-
    /// Doesn't have to be unique on the network.
-
    pub alias: Option<String>,
-
    /// Peers to connect to on startup.
-
    /// Connections to these peers will be maintained.
-
    pub connect: Vec<(NodeId, Address)>,
-
    /// Specify the node's public addresses
-
    pub external_addresses: Vec<Address>,
-
    /// Peer-to-peer network.
-
    pub network: Network,
-
    /// Whether or not our node should relay inventories.
-
    pub relay: bool,
-
    /// Configured service limits.
-
    pub limits: Limits,
-
    /// Default tracking policy.
-
    pub policy: Policy,
-
    /// Default tracking scope.
-
    pub scope: Scope,
-
}
-

-
impl Default for Config {
-
    fn default() -> Self {
-
        Self {
-
            alias: None,
-
            connect: Vec::default(),
-
            external_addresses: vec![],
-
            network: Network::default(),
-
            relay: true,
-
            limits: Limits::default(),
-
            policy: Policy::default(),
-
            scope: Scope::default(),
-
        }
-
    }
-
}
-

-
impl Config {
-
    pub fn new(network: Network) -> Self {
-
        Self {
-
            network,
-
            ..Self::default()
-
        }
-
    }
-

-
    pub fn peer(&self, id: &NodeId) -> Option<&Address> {
-
        self.connect.iter().find(|(i, _)| i == id).map(|(_, a)| a)
-
    }
-

-
    pub fn is_persistent(&self, id: &NodeId) -> bool {
-
        self.connect.iter().any(|(i, _)| i == id)
-
    }
-

-
    pub fn features(&self) -> node::Features {
-
        node::Features::SEED
-
    }
-

-
    /// Check if a node announcement matches this configuration.
-
    pub fn matches(&self, other: &NodeAnnouncement) -> bool {
-
        let ann = self.node(other.timestamp);
-

-
        ann.features == other.features
-
            && ann.alias == other.alias
-
            && ann.addresses == other.addresses
-
    }
-

-
    pub fn alias(&self) -> [u8; 32] {
-
        let mut alias = [0u8; 32];
-

-
        if let Some(name) = &self.alias {
-
            alias[..name.len()].copy_from_slice(name.as_bytes());
-
        }
-
        alias
-
    }
-

-
    pub fn node(&self, timestamp: node::Timestamp) -> NodeAnnouncement {
-
        let features = self.features();
-
        let alias = self.alias();
-
        let addresses: BoundedVec<_, ADDRESS_LIMIT> = self
-
            .external_addresses
-
            .clone()
-
            .try_into()
-
            .expect("external addresses are within the limit");
-

-
        NodeAnnouncement {
-
            features,
-
            timestamp,
-
            alias,
-
            addresses,
-
            nonce: 0,
-
        }
-
    }
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-

-
    #[test]
-
    fn test_node_announcement() {
-
        let cfg = Config {
-
            alias: Some(String::from("cloudhead")),
-
            ..Config::default()
-
        };
-
        assert_eq!("cloudhead", cfg.node(0).alias().unwrap());
-
    }
-
}
modified radicle-node/src/service/message.rs
@@ -1,10 +1,10 @@
-
use std::{fmt, io, mem, str};
+
use std::{fmt, io, mem};

use crate::crypto;
use crate::crypto::Unverified;
use crate::identity::Id;
use crate::node;
-
use crate::node::Address;
+
use crate::node::{Address, Alias};
use crate::prelude::BoundedVec;
use crate::service::filter::Filter;
use crate::service::{Link, NodeId, Timestamp};
@@ -57,8 +57,8 @@ pub struct NodeAnnouncement {
    pub features: node::Features,
    /// Monotonic timestamp.
    pub timestamp: Timestamp,
-
    /// Non-unique alias. Must be valid UTF-8.
-
    pub alias: [u8; 32],
+
    /// Non-unique alias.
+
    pub alias: Alias,
    /// Announced addresses.
    pub addresses: BoundedVec<Address, ADDRESS_LIMIT>,
    /// Nonce used for announcement proof-of-work.
@@ -118,11 +118,6 @@ impl NodeAnnouncement {
        }
        Some(self)
    }
-

-
    /// Get the alias as a UTF-8 string.
-
    pub fn alias(&self) -> Result<&str, std::str::Utf8Error> {
-
        Ok(str::from_utf8(&self.alias)?.trim_end_matches(0 as char))
-
    }
}

impl wire::Encode for NodeAnnouncement {
@@ -619,7 +614,7 @@ mod tests {
        let ann = NodeAnnouncement {
            features: node::Features::SEED,
            timestamp: 42491841,
-
            alias: [0; 32],
+
            alias: Alias::new("alice"),
            addresses: BoundedVec::new(),
            nonce: 0,
        };
@@ -627,6 +622,6 @@ mod tests {
        assert_eq!(ann.work(), 0);
        assert_eq!(ann.clone().solve(1).unwrap().work(), 4);
        assert_eq!(ann.clone().solve(8).unwrap().work(), 9);
-
        assert_eq!(ann.solve(14).unwrap().work(), 16);
+
        assert_eq!(ann.solve(14).unwrap().work(), 14);
    }
}
modified radicle-node/src/service/session.rs
@@ -1,7 +1,7 @@
use std::collections::{HashSet, VecDeque};
use std::fmt;

-
use crate::service::config::Limits;
+
use crate::node::config::Limits;
use crate::service::message;
use crate::service::message::Message;
use crate::service::{Address, Id, LocalTime, NodeId, Outbox, Rng};
modified radicle-node/src/test/arbitrary.rs
@@ -2,6 +2,7 @@ use bloomy::BloomFilter;
use qcheck::Arbitrary;

use crate::crypto;
+
use crate::node::Alias;
use crate::prelude::{BoundedVec, Id, NodeId, Timestamp};
use crate::service::filter::{Filter, FILTER_SIZE_L, FILTER_SIZE_M, FILTER_SIZE_S};
use crate::service::message::{
@@ -65,7 +66,7 @@ impl Arbitrary for Message {
                let message = NodeAnnouncement {
                    features: u64::arbitrary(g).into(),
                    timestamp: Timestamp::arbitrary(g),
-
                    alias: <[u8; 32]>::arbitrary(g),
+
                    alias: Alias::arbitrary(g),
                    addresses: Arbitrary::arbitrary(g),
                    nonce: u64::arbitrary(g),
                }
modified radicle-node/src/test/environment.rs
@@ -1,6 +1,7 @@
use std::io::BufRead as _;
use std::mem::ManuallyDrop;
use std::path::{Path, PathBuf};
+
use std::str::FromStr;
use std::{
    collections::{BTreeMap, BTreeSet},
    env, fs, io, iter, net, process, thread, time,
@@ -21,7 +22,8 @@ use radicle::node::address::Book;
use radicle::node::routing::Store;
use radicle::node::tracking::store as TrackingStore;
use radicle::node::Handle as _;
-
use radicle::node::{ADDRESS_DB_FILE, TRACKING_DB_FILE};
+
use radicle::node::{Alias, ADDRESS_DB_FILE, TRACKING_DB_FILE};
+
use radicle::profile;
use radicle::profile::Home;
use radicle::profile::Profile;
use radicle::rad;
@@ -74,8 +76,8 @@ impl Environment {

    /// Create a new node in this environment. This should be used when a running node
    /// is required. Use [`Environment::profile`] otherwise.
-
    pub fn node(&mut self, name: &str) -> Node<MemorySigner> {
-
        let profile = self.profile(name);
+
    pub fn node(&mut self, config: Config) -> Node<MemorySigner> {
+
        let profile = self.profile(&config.alias);
        let signer = MemorySigner::load(&profile.keystore, "radicle".to_owned().into()).unwrap();
        let tracking_db = profile.home.node().join(TRACKING_DB_FILE);
        TrackingStore::Config::open(tracking_db).unwrap();
@@ -85,6 +87,7 @@ impl Environment {
        Node {
            id: *profile.id(),
            home: profile.home,
+
            config,
            signer,
            storage: profile.storage,
        }
@@ -92,12 +95,15 @@ impl Environment {

    /// Create a new profile in this environment.
    /// This should be used when a running node is not required.
-
    pub fn profile(&mut self, name: &str) -> Profile {
-
        let home = Home::new(self.tmp().join("home").join(name).join(".radicle")).unwrap();
+
    pub fn profile(&mut self, alias: &str) -> Profile {
+
        let home = Home::new(self.tmp().join("home").join(alias).join(".radicle")).unwrap();
        let storage = Storage::open(home.storage()).unwrap();
        let keystore = Keystore::new(&home.keys());
        let keypair = KeyPair::from_seed(Seed::from([!(self.users as u8); 32]));
        let tracking_db = home.node().join(TRACKING_DB_FILE);
+
        let alias = Alias::from_str(alias).unwrap();
+
        let config = profile::Config::init(alias, &home.config()).unwrap();
+

        TrackingStore::Config::open(tracking_db).unwrap();
        let addresses_db = home.node().join(ADDRESS_DB_FILE);
        Book::open(addresses_db).unwrap();
@@ -115,6 +121,7 @@ impl Environment {
            storage,
            keystore,
            public_key: keypair.pk.into(),
+
            config,
        }
    }
}
@@ -125,6 +132,7 @@ pub struct Node<G> {
    pub home: Home,
    pub signer: G,
    pub storage: Storage,
+
    pub config: Config,
}

/// Handle to a running node.
@@ -303,7 +311,7 @@ impl<G: Signer + cyphernet::Ecdh> NodeHandle<G> {

impl Node<MockSigner> {
    /// Create a new node.
-
    pub fn init(base: &Path) -> Self {
+
    pub fn init(base: &Path, config: Config) -> Self {
        let home = base.join(
            iter::repeat_with(fastrand::alphanumeric)
                .take(8)
@@ -318,13 +326,14 @@ impl Node<MockSigner> {
            home,
            signer,
            storage,
+
            config,
        }
    }
}

impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer + Clone> Node<G> {
    /// Spawn a node in its own thread.
-
    pub fn spawn(self, config: service::Config) -> NodeHandle<G> {
+
    pub fn spawn(self) -> NodeHandle<G> {
        let listen = vec![([0, 0, 0, 0], 0).into()];
        let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050);
        let daemon: net::SocketAddr = {
@@ -337,7 +346,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer + Clone> Node<G> {
        let (_, signals) = chan::bounded(1);
        let rt = Runtime::init(
            self.home.clone(),
-
            config,
+
            self.config,
            listen,
            proxy,
            daemon,
modified radicle-node/src/test/handle.rs
@@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex};
use std::{io, time};

use crate::identity::Id;
-
use crate::node::{Event, FetchResult, Seeds};
+
use crate::node::{Alias, Event, FetchResult, Seeds};
use crate::runtime::HandleError;
use crate::service::tracking;
use crate::service::NodeId;
@@ -51,7 +51,7 @@ impl radicle::node::Handle for Handle {
        Ok(self.tracking_repos.lock().unwrap().remove(&id))
    }

-
    fn track_node(&mut self, id: NodeId, _alias: Option<String>) -> Result<bool, Self::Error> {
+
    fn track_node(&mut self, id: NodeId, _alias: Option<Alias>) -> Result<bool, Self::Error> {
        Ok(self.tracking_nodes.lock().unwrap().insert(id))
    }

modified radicle-node/src/test/peer.rs
@@ -2,11 +2,12 @@
use std::iter;
use std::net;
use std::ops::{Deref, DerefMut};
+
use std::str::FromStr;

use log::*;

-
use radicle::node::address;
use radicle::node::address::Store;
+
use radicle::node::{address, Alias};
use radicle::rad;
use radicle::storage::ReadRepository;
use radicle::Storage;
@@ -110,7 +111,7 @@ impl Default for Config<MockSigner> {
        let signer = MockSigner::new(&mut rng);

        Config {
-
            config: service::Config::default(),
+
            config: service::Config::new(Alias::from_str("mocky").unwrap()),
            addrs: address::Book::memory().unwrap(),
            local_time: LocalTime::now(),
            policy: Policy::default(),
@@ -162,7 +163,7 @@ where
        // Make sure the peer address is advertized.
        config.config.external_addresses.push(local_addr.into());

-
        let announcement = config.config.node(config.local_time.as_secs());
+
        let announcement = service::gossip::node(&config.config, config.local_time.as_secs());
        let emitter: Emitter<Event> = Default::default();
        let service = Service::new(
            config.config,
@@ -214,7 +215,7 @@ where
                .insert(
                    &peer.node_id(),
                    radicle::node::Features::default(),
-
                    peer.name,
+
                    Alias::from_str(peer.name).unwrap(),
                    0,
                    timestamp,
                    Some(known_address),
@@ -258,14 +259,11 @@ where
    }

    pub fn node_announcement(&self) -> Message {
-
        let mut alias = [0u8; 32];
-
        alias[..self.name.len()].copy_from_slice(self.name.as_bytes());
-

        Message::node(
            NodeAnnouncement {
                features: node::Features::SEED,
                timestamp: self.timestamp(),
-
                alias,
+
                alias: Alias::from_str(self.name).unwrap(),
                addresses: Some(net::SocketAddr::from((self.ip, node::DEFAULT_PORT)).into()).into(),
                nonce: 0,
            }
modified radicle-node/src/tests.rs
@@ -14,9 +14,9 @@ use crate::collections::{HashMap, HashSet};
use crate::crypto::test::signer::MockSigner;
use crate::identity::Id;
use crate::node;
+
use crate::node::config::*;
use crate::prelude::*;
use crate::prelude::{LocalDuration, Timestamp};
-
use crate::service::config::*;
use crate::service::filter::Filter;
use crate::service::io::Io;
use crate::service::message::*;
@@ -208,8 +208,11 @@ fn test_persistent_peer_connect() {
        MockStorage::empty(),
        peer::Config {
            config: Config {
-
                connect: vec![(bob.id(), bob.address()), (eve.id(), eve.address())],
-
                ..Config::default()
+
                connect: vec![
+
                    (bob.id(), bob.address()).into(),
+
                    (eve.id(), eve.address()).into(),
+
                ],
+
                ..Config::new(node::Alias::new("alice"))
            },
            ..peer::Config::default()
        },
@@ -320,7 +323,7 @@ fn test_inventory_pruning() {
            peer::Config {
                config: Config {
                    limits: test.limits,
-
                    ..Config::default()
+
                    ..Config::new(node::Alias::new("alice"))
                },
                ..peer::Config::default()
            },
@@ -753,7 +756,11 @@ fn test_refs_announcement_trusted() {

    // Alice starts to track Bob.
    let (sender, receiver) = chan::bounded(1);
-
    alice.command(Command::TrackNode(bob.id, Some("bob".to_string()), sender));
+
    alice.command(Command::TrackNode(
+
        bob.id,
+
        Some(node::Alias::new("bob")),
+
        sender,
+
    ));
    let policy_change = receiver.recv().map_err(runtime::HandleError::from).unwrap();
    assert!(policy_change);

@@ -886,8 +893,11 @@ fn test_persistent_peer_reconnect_attempt() {
        MockStorage::empty(),
        peer::Config {
            config: Config {
-
                connect: vec![(bob.id(), bob.address()), (eve.id(), eve.address())],
-
                ..Config::default()
+
                connect: vec![
+
                    (bob.id(), bob.address()).into(),
+
                    (eve.id(), eve.address()).into(),
+
                ],
+
                ..Config::new(node::Alias::new("alice"))
            },
            ..peer::Config::default()
        },
@@ -944,8 +954,8 @@ fn test_persistent_peer_reconnect_success() {
        MockStorage::empty(),
        peer::Config {
            config: Config {
-
                connect: vec![(bob.id, bob.addr())],
-
                ..Config::default()
+
                connect: vec![(bob.id, bob.addr()).into()],
+
                ..Config::new(node::Alias::new("alice"))
            },
            ..peer::Config::default()
        },
modified radicle-node/src/tests/e2e.rs
@@ -2,13 +2,14 @@ use std::{collections::HashSet, thread, time};

use radicle::crypto::{test::signer::MockSigner, Signer};
use radicle::git;
-
use radicle::node::{FetchResult, Handle as _};
+
use radicle::node::{Alias, FetchResult, Handle as _};
use radicle::storage::{ReadRepository, ReadStorage, WriteRepository, WriteStorage};
use radicle::test::fixtures;
use radicle::{assert_matches, rad};

+
use crate::node::config::Limits;
+
use crate::node::Config;
use crate::service;
-
use crate::service::config::Limits;
use crate::service::tracking::Scope;
use crate::storage::git::transport;
use crate::test::environment::{converge, Environment, Node};
@@ -23,14 +24,14 @@ fn test_inventory_sync_basic() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::init(tmp.path());
-
    let mut bob = Node::init(tmp.path());
+
    let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));

    alice.project("alice", "");
    bob.project("bob", "");

-
    let mut alice = alice.spawn(service::Config::default());
-
    let bob = bob.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let bob = bob.spawn();

    alice.connect(&bob);

@@ -47,17 +48,17 @@ fn test_inventory_sync_bridge() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::init(tmp.path());
-
    let mut bob = Node::init(tmp.path());
-
    let mut eve = Node::init(tmp.path());
+
    let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
+
    let mut eve = Node::init(tmp.path(), Config::new(Alias::new("eve")));

    alice.project("alice", "");
    bob.project("bob", "");
    eve.project("eve", "");

-
    let mut alice = alice.spawn(service::Config::default());
-
    let mut bob = bob.spawn(service::Config::default());
-
    let eve = eve.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let eve = eve.spawn();

    alice.connect(&bob);
    bob.connect(&eve);
@@ -77,20 +78,20 @@ fn test_inventory_sync_ring() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::init(tmp.path());
-
    let mut bob = Node::init(tmp.path());
-
    let mut eve = Node::init(tmp.path());
-
    let mut carol = Node::init(tmp.path());
+
    let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
+
    let mut eve = Node::init(tmp.path(), Config::new(Alias::new("eve")));
+
    let mut carol = Node::init(tmp.path(), Config::new(Alias::new("carol")));

    alice.project("alice", "");
    bob.project("bob", "");
    eve.project("eve", "");
    carol.project("carol", "");

-
    let mut alice = alice.spawn(service::Config::default());
-
    let mut bob = bob.spawn(service::Config::default());
-
    let mut eve = eve.spawn(service::Config::default());
-
    let mut carol = carol.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let mut eve = eve.spawn();
+
    let mut carol = carol.spawn();

    alice.connect(&bob);
    bob.connect(&eve);
@@ -114,11 +115,11 @@ fn test_inventory_sync_star() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::init(tmp.path());
-
    let mut bob = Node::init(tmp.path());
-
    let mut eve = Node::init(tmp.path());
-
    let mut carol = Node::init(tmp.path());
-
    let mut dave = Node::init(tmp.path());
+
    let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
+
    let mut eve = Node::init(tmp.path(), Config::new(Alias::new("eve")));
+
    let mut carol = Node::init(tmp.path(), Config::new(Alias::new("carol")));
+
    let mut dave = Node::init(tmp.path(), Config::new(Alias::new("dave")));

    alice.project("alice", "");
    bob.project("bob", "");
@@ -126,11 +127,11 @@ fn test_inventory_sync_star() {
    carol.project("carol", "");
    dave.project("dave", "");

-
    let alice = alice.spawn(service::Config::default());
-
    let mut bob = bob.spawn(service::Config::default());
-
    let mut eve = eve.spawn(service::Config::default());
-
    let mut carol = carol.spawn(service::Config::default());
-
    let mut dave = dave.spawn(service::Config::default());
+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let mut eve = eve.spawn();
+
    let mut carol = carol.spawn();
+
    let mut dave = dave.spawn();

    bob.connect(&alice);
    eve.connect(&alice);
@@ -146,12 +147,12 @@ fn test_replication() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::init(tmp.path());
-
    let mut bob = Node::init(tmp.path());
+
    let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
    let acme = bob.project("acme", "");

-
    let mut alice = alice.spawn(service::Config::default());
-
    let bob = bob.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let bob = bob.spawn();

    alice.connect(&bob);
    converge([&alice, &bob]);
@@ -203,8 +204,8 @@ fn test_replication_no_delegates() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::init(tmp.path());
-
    let mut bob = Node::init(tmp.path());
+
    let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));

    let acme = bob.project("acme", "");
    // Delete one of the signed refs.
@@ -216,8 +217,8 @@ fn test_replication_no_delegates() {
        .delete()
        .unwrap();

-
    let mut alice = alice.spawn(service::Config::default());
-
    let bob = bob.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let bob = bob.spawn();

    alice.connect(&bob);
    converge([&alice, &bob]);
@@ -236,8 +237,8 @@ fn test_replication_no_delegates() {
#[test]
fn test_replication_invalid() {
    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::init(tmp.path());
-
    let mut bob = Node::init(tmp.path());
+
    let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
    let carol = MockSigner::default();
    let acme = bob.project("acme", "");
    let repo = bob.storage.repository_mut(acme).unwrap();
@@ -262,8 +263,8 @@ fn test_replication_invalid() {
        )
        .unwrap();

-
    let mut alice = alice.spawn(service::Config::default());
-
    let bob = bob.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let bob = bob.spawn();

    alice.connect(&bob);
    converge([&alice, &bob]);
@@ -289,12 +290,12 @@ fn test_migrated_clone() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let mut alice = Node::init(tmp.path());
-
    let bob = Node::init(tmp.path());
+
    let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
    let acme = alice.project("acme", "");

-
    let mut alice = alice.spawn(service::Config::default());
-
    let mut bob = bob.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();

    alice.connect(&bob);
    converge([&alice, &bob]);
@@ -339,12 +340,12 @@ fn test_dont_fetch_owned_refs() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let mut alice = Node::init(tmp.path());
-
    let bob = Node::init(tmp.path());
+
    let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
    let acme = alice.project("acme", "");

-
    let mut alice = alice.spawn(service::Config::default());
-
    let mut bob = bob.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();

    alice.connect(&bob);
    converge([&alice, &bob]);
@@ -366,8 +367,8 @@ fn test_fetch_trusted_remotes() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let mut alice = Node::init(tmp.path());
-
    let bob = Node::init(tmp.path());
+
    let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
    let acme = alice.project("acme", "");
    let mut signers = Vec::with_capacity(5);
    {
@@ -378,8 +379,8 @@ fn test_fetch_trusted_remotes() {
        }
    }

-
    let mut alice = alice.spawn(service::Config::default());
-
    let mut bob = bob.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();

    alice.connect(&bob);
    converge([&alice, &bob]);
@@ -421,12 +422,12 @@ fn test_missing_remote() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let mut alice = Node::init(tmp.path());
-
    let bob = Node::init(tmp.path());
+
    let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
    let acme = alice.project("acme", "");

-
    let mut alice = alice.spawn(service::Config::default());
-
    let mut bob = bob.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
    let carol = MockSigner::default();

    alice.connect(&bob);
@@ -450,11 +451,11 @@ fn test_fetch_preserve_owned_refs() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let mut alice = Node::init(tmp.path());
-
    let bob = Node::init(tmp.path());
+
    let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
    let acme = alice.project("acme", "");
-
    let mut alice = alice.spawn(service::Config::default());
-
    let mut bob = bob.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();

    alice.connect(&bob);
    converge([&alice, &bob]);
@@ -496,12 +497,12 @@ fn test_clone() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::init(tmp.path());
-
    let mut bob = Node::init(tmp.path());
+
    let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
    let acme = bob.project("acme", "");

-
    let mut alice = alice.spawn(service::Config::default());
-
    let bob = bob.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let bob = bob.spawn();

    alice.connect(&bob);
    converge([&alice, &bob]);
@@ -554,12 +555,12 @@ fn test_fetch_up_to_date() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::init(tmp.path());
-
    let mut bob = Node::init(tmp.path());
+
    let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
    let acme = bob.project("acme", "");

-
    let mut alice = alice.spawn(service::Config::default());
-
    let bob = bob.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let bob = bob.spawn();

    alice.connect(&bob);
    converge([&alice, &bob]);
@@ -584,8 +585,8 @@ fn test_large_fetch() {

    let env = Environment::new();
    let scale = env.scale();
-
    let mut alice = Node::init(&env.tmp());
-
    let bob = Node::init(&env.tmp());
+
    let mut alice = Node::init(&env.tmp(), Config::new(Alias::new("alice")));
+
    let bob = Node::init(&env.tmp(), Config::new(Alias::new("bob")));

    let tmp = tempfile::tempdir().unwrap();
    let (repo, _) = fixtures::repository(tmp.path());
@@ -593,8 +594,8 @@ fn test_large_fetch() {

    let rid = alice.project_from("acme", "", &repo);

-
    let mut alice = alice.spawn(service::Config::default());
-
    let mut bob = bob.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
    let bob_events = bob.handle.events();

    bob.handle.track_repo(rid, Scope::All).unwrap();
@@ -619,11 +620,28 @@ fn test_concurrent_fetches() {

    let env = Environment::new();
    let scale = env.scale();
+
    let repos = scale.max(4);
+
    let limits = Limits {
+
        // Have one fetch be queued.
+
        fetch_concurrency: repos - 1,
+
        ..Limits::default()
+
    };
    let mut bob_repos = HashSet::new();
    let mut alice_repos = HashSet::new();
-
    let mut alice = Node::init(&env.tmp());
-
    let mut bob = Node::init(&env.tmp());
-
    let repos = scale.max(4);
+
    let mut alice = Node::init(
+
        &env.tmp(),
+
        service::Config {
+
            limits: limits.clone(),
+
            ..service::Config::new(Alias::new("alice"))
+
        },
+
    );
+
    let mut bob = Node::init(
+
        &env.tmp(),
+
        service::Config {
+
            limits,
+
            ..service::Config::new(Alias::new("bob"))
+
        },
+
    );

    for i in 0..repos {
        // Create a repo for Alice.
@@ -643,16 +661,8 @@ fn test_concurrent_fetches() {
        bob_repos.insert(rid);
    }

-
    let config = service::Config {
-
        limits: Limits {
-
            // Have one fetch be queued.
-
            fetch_concurrency: repos - 1,
-
            ..Limits::default()
-
        },
-
        ..service::Config::default()
-
    };
-
    let mut alice = alice.spawn(config.clone());
-
    let mut bob = bob.spawn(config);
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();

    let alice_events = alice.handle.events();
    let bob_events = bob.handle.events();
@@ -718,11 +728,11 @@ fn test_connection_crossing() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::init(tmp.path());
-
    let bob = Node::init(tmp.path());
+
    let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));

-
    let mut alice = alice.spawn(service::Config::default());
-
    let mut bob = bob.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();

    alice.handle.connect(bob.id, bob.addr.into()).unwrap();
    bob.handle.connect(alice.id, alice.addr.into()).unwrap();
@@ -753,15 +763,15 @@ fn test_non_fastforward_sigrefs() {

    let tmp = tempfile::tempdir().unwrap();

-
    let alice = Node::init(tmp.path());
-
    let mut bob = Node::init(tmp.path());
-
    let eve = Node::init(tmp.path());
+
    let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
+
    let eve = Node::init(tmp.path(), Config::new(Alias::new("eve")));

    let rid = bob.project("acme", "");

-
    let mut alice = alice.spawn(service::Config::default());
-
    let bob = bob.spawn(service::Config::default());
-
    let mut eve = eve.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let bob = bob.spawn();
+
    let mut eve = eve.spawn();

    alice.handle.track_repo(rid, Scope::All).unwrap();
    eve.handle.track_repo(rid, Scope::All).unwrap();
@@ -805,15 +815,15 @@ fn test_outdated_sigrefs() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::init(tmp.path());
-
    let bob = Node::init(tmp.path());
-
    let eve = Node::init(tmp.path());
+
    let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
+
    let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
+
    let eve = Node::init(tmp.path(), Config::new(Alias::new("eve")));

    let rid = alice.project("acme", "");

-
    let mut alice = alice.spawn(service::Config::default());
-
    let mut bob = bob.spawn(service::Config::default());
-
    let mut eve = eve.spawn(service::Config::default());
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let mut eve = eve.spawn();

    bob.handle.track_repo(rid, Scope::All).unwrap();
    eve.handle.track_repo(rid, Scope::All).unwrap();
@@ -832,7 +842,7 @@ fn test_outdated_sigrefs() {

    alice
        .handle
-
        .track_node(eve.id, Some("eve".to_string()))
+
        .track_node(eve.id, Some(Alias::new("eve")))
        .unwrap();
    alice.handle.fetch(rid, eve.id).unwrap();
    let repo = alice.storage.repository(rid).unwrap();
@@ -873,7 +883,7 @@ fn test_outdated_sigrefs() {

    alice
        .handle
-
        .track_node(bob.id, Some("bob".to_string()))
+
        .track_node(bob.id, Some(Alias::new("bob")))
        .unwrap();
    assert_matches!(
        alice.handle.fetch(rid, bob.id).unwrap(),
modified radicle-node/src/wire.rs
@@ -10,6 +10,7 @@ pub use protocol::{Control, Wire, WireReader, WireSession, WireWriter};
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::ops::Deref;
+
use std::str::FromStr;
use std::string::FromUtf8Error;
use std::{io, mem};

@@ -20,6 +21,7 @@ use crate::git;
use crate::git::fmt;
use crate::identity::Id;
use crate::node;
+
use crate::node::Alias;
use crate::prelude::*;
use crate::service::filter;
use crate::storage::refs::Refs;
@@ -47,6 +49,8 @@ pub enum Error {
    InvalidStreamKind(u8),
    #[error(transparent)]
    InvalidRefName(#[from] fmt::Error),
+
    #[error(transparent)]
+
    InvalidAlias(#[from] node::AliasError),
    #[error("invalid control message with type `{0}`")]
    InvalidControlMessage(u8),
    #[error("invalid protocol version header `{0:x?}`")]
@@ -219,6 +223,12 @@ impl Encode for Refs {
    }
}

+
impl Encode for Alias {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        self.as_ref().encode(writer)
+
    }
+
}
+

impl<A, B> Encode for (A, B)
where
    A: Encode,
@@ -284,6 +294,12 @@ impl Decode for git::RefString {
    }
}

+
impl Decode for Alias {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        String::decode(reader).and_then(|s| Alias::from_str(&s).map_err(Error::from))
+
    }
+
}
+

impl<A, B> Decode for (A, B)
where
    A: Decode,
@@ -574,6 +590,14 @@ mod tests {
    }

    #[test]
+
    fn test_alias() {
+
        assert_eq!(
+
            serialize(&Alias::from_str("hello").unwrap()),
+
            vec![5, b'h', b'e', b'l', b'l', b'o']
+
        );
+
    }
+

+
    #[test]
    fn test_filter_invalid() {
        let b = bloomy::BloomFilter::with_size(filter::FILTER_SIZE_M / 3);
        let f = filter::Filter::from(b);
modified radicle-term/src/io.rs
@@ -30,6 +30,7 @@ pub static CONFIG: Lazy<RenderConfig> = Lazy::new(|| RenderConfig {
    answer: StyleSheet::new(),
    highlighted_option_prefix: Styled::new("*").with_fg(Color::LightYellow),
    help_message: StyleSheet::new().with_fg(Color::DarkGrey),
+
    default_value: StyleSheet::new().with_fg(Color::LightBlue),
    error_message: ErrorMessageRenderConfig::default_colored()
        .with_prefix(Styled::new("โœ—").with_fg(Color::LightRed)),
    ..RenderConfig::default_colored()
@@ -173,16 +174,18 @@ pub fn abort<D: fmt::Display>(prompt: D) -> bool {
    ask(prompt, false)
}

-
pub fn input<S, E>(message: &str, default: Option<S>) -> anyhow::Result<S>
+
pub fn input<S, E>(message: &str, default: Option<S>, help: Option<&str>) -> anyhow::Result<S>
where
    S: fmt::Display + std::str::FromStr<Err = E> + Clone,
    E: fmt::Debug + fmt::Display,
{
-
    let input = CustomType::<S>::new(message).with_render_config(*CONFIG);
-
    let value = match default {
-
        Some(default) => input.with_default(default).prompt()?,
-
        None => input.prompt()?,
-
    };
+
    let mut input = CustomType::<S>::new(message).with_render_config(*CONFIG);
+

+
    input.default = default;
+
    input.help_message = help;
+

+
    let value = input.prompt()?;
+

    Ok(value)
}

modified radicle-tools/Cargo.toml
@@ -24,10 +24,6 @@ name = "rad-init"
path = "src/rad-init.rs"

[[bin]]
-
name = "rad-auth"
-
path = "src/rad-auth.rs"
-

-
[[bin]]
name = "rad-self"
path = "src/rad-self.rs"

deleted radicle-tools/src/rad-auth.rs
@@ -1,15 +0,0 @@
-
use radicle::profile;
-
use radicle::profile::{Error, Profile};
-

-
fn main() -> anyhow::Result<()> {
-
    let profile = match Profile::load() {
-
        Ok(profile) => profile,
-
        Err(Error::NotFound(_)) => Profile::init(profile::home()?, "radicle".to_owned())?,
-
        Err(err) => anyhow::bail!(err),
-
    };
-

-
    println!("id: {}", profile.id());
-
    println!("home: {}", profile.home().display());
-

-
    Ok(())
-
}
modified radicle/src/lib.rs
@@ -31,7 +31,7 @@ pub mod prelude {

    pub use crypto::{PublicKey, Signer, Verified};
    pub use identity::{project::Project, Did, Doc, Id};
-
    pub use node::{NodeId, Timestamp};
+
    pub use node::{Alias, NodeId, Timestamp};
    pub use profile::Profile;
    pub use storage::{BranchName, ReadRepository, ReadStorage, WriteRepository, WriteStorage};
}
modified radicle/src/node.rs
@@ -1,6 +1,7 @@
mod features;

pub mod address;
+
pub mod config;
pub mod events;
pub mod routing;
pub mod tracking;
@@ -10,6 +11,7 @@ use std::io::{BufRead, BufReader};
use std::ops::Deref;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
+
use std::str::FromStr;
use std::{fmt, io, net, thread, time};

use amplify::WrapperMut;
@@ -23,6 +25,7 @@ use crate::crypto::PublicKey;
use crate::identity::Id;
use crate::storage::RefUpdate;

+
pub use config::Config;
pub use cyphernet::addr::PeerAddr;
pub use events::{Event, Events};
pub use features::Features;
@@ -33,6 +36,8 @@ pub const DEFAULT_SOCKET_NAME: &str = "control.sock";
pub const DEFAULT_PORT: u16 = 8776;
/// Default timeout when waiting for the node to respond with data.
pub const DEFAULT_TIMEOUT: time::Duration = time::Duration::from_secs(9);
+
/// Maximum length in bytes of a node alias.
+
pub const MAX_ALIAS_LENGTH: usize = 32;
/// Filename of routing table database under the node directory.
pub const ROUTING_DB_FILE: &str = "routing.db";
/// Filename of address database under the node directory.
@@ -107,6 +112,90 @@ impl fmt::Display for State {
    }
}

+
/// Node alias.
+
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
+
pub struct Alias(String);
+

+
impl Alias {
+
    /// Create a new alias from a string. Panics if the string is not a valid alias.
+
    pub fn new(alias: impl ToString) -> Self {
+
        let alias = alias.to_string();
+

+
        match Self::from_str(&alias) {
+
            Ok(a) => a,
+
            Err(e) => panic!("Alias::new: {e}"),
+
        }
+
    }
+
}
+

+
impl From<Alias> for String {
+
    fn from(value: Alias) -> Self {
+
        value.0
+
    }
+
}
+

+
impl From<&NodeId> for Alias {
+
    fn from(nid: &NodeId) -> Self {
+
        Alias(nid.to_string())
+
    }
+
}
+

+
impl fmt::Display for Alias {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        self.0.fmt(f)
+
    }
+
}
+

+
impl Deref for Alias {
+
    type Target = str;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl AsRef<str> for Alias {
+
    fn as_ref(&self) -> &str {
+
        self.0.as_str()
+
    }
+
}
+

+
impl From<&Alias> for [u8; 32] {
+
    fn from(input: &Alias) -> [u8; 32] {
+
        let mut alias = [0u8; 32];
+

+
        alias[..input.len()].copy_from_slice(input.as_bytes());
+
        alias
+
    }
+
}
+

+
#[derive(thiserror::Error, Debug)]
+
pub enum AliasError {
+
    #[error("alias cannot be empty")]
+
    Empty,
+
    #[error("alias cannot be greater than {MAX_ALIAS_LENGTH} bytes")]
+
    MaxBytesExceeded,
+
    #[error("alias cannot contain whitespace or control characters")]
+
    InvalidCharacter,
+
}
+

+
impl FromStr for Alias {
+
    type Err = AliasError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        if s.is_empty() {
+
            return Err(AliasError::Empty);
+
        }
+
        if s.chars().any(|c| c.is_control() || c.is_whitespace()) {
+
            return Err(AliasError::InvalidCharacter);
+
        }
+
        if s.len() > MAX_ALIAS_LENGTH {
+
            return Err(AliasError::MaxBytesExceeded);
+
        }
+
        Ok(Self(s.to_owned()))
+
    }
+
}
+

/// Result of a command, on the node control socket.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "status")]
@@ -499,7 +588,7 @@ pub trait Handle: Clone + Sync + Send {
    /// tracked.
    fn track_repo(&mut self, id: Id, scope: tracking::Scope) -> Result<bool, Self::Error>;
    /// Start tracking the given node.
-
    fn track_node(&mut self, id: NodeId, alias: Option<String>) -> Result<bool, Self::Error>;
+
    fn track_node(&mut self, id: NodeId, alias: Option<Alias>) -> Result<bool, Self::Error>;
    /// Untrack the given project and delete it from storage.
    fn untrack_repo(&mut self, id: Id) -> Result<bool, Self::Error>;
    /// Untrack the given node.
@@ -671,7 +760,7 @@ impl Handle for Node {
        Ok(result)
    }

-
    fn track_node(&mut self, id: NodeId, alias: Option<String>) -> Result<bool, Error> {
+
    fn track_node(&mut self, id: NodeId, alias: Option<Alias>) -> Result<bool, Error> {
        let id = id.to_human();
        let args = if let Some(alias) = alias.as_deref() {
            vec![id.as_str(), alias]
@@ -787,23 +876,23 @@ impl Handle for Node {
/// A trait for different sources which can potentially return an alias.
pub trait AliasStore {
    /// Returns alias of a `NodeId`.
-
    fn alias(&self, nid: &NodeId) -> Option<String>;
+
    fn alias(&self, nid: &NodeId) -> Option<Alias>;
}

impl<T: AliasStore + ?Sized> AliasStore for &T {
-
    fn alias(&self, nid: &NodeId) -> Option<String> {
+
    fn alias(&self, nid: &NodeId) -> Option<Alias> {
        (*self).alias(nid)
    }
}

impl<T: AliasStore + ?Sized> AliasStore for Box<T> {
-
    fn alias(&self, nid: &NodeId) -> Option<String> {
+
    fn alias(&self, nid: &NodeId) -> Option<Alias> {
        self.deref().alias(nid)
    }
}

-
impl AliasStore for HashMap<NodeId, String> {
-
    fn alias(&self, nid: &NodeId) -> Option<String> {
+
impl AliasStore for HashMap<NodeId, Alias> {
+
    fn alias(&self, nid: &NodeId) -> Option<Alias> {
        self.get(nid).map(ToOwned::to_owned)
    }
}
@@ -816,4 +905,19 @@ mod test {
    fn test_command_name_display() {
        assert_eq!(CommandName::TrackNode.to_string(), "track-node");
    }
+

+
    #[test]
+
    fn test_alias() {
+
        assert!(Alias::from_str("cloudhead").is_ok());
+
        assert!(Alias::from_str("cloud-head").is_ok());
+
        assert!(Alias::from_str("cl0ud.h3ad$__").is_ok());
+
        assert!(Alias::from_str("ยฉloudhรจรขd").is_ok());
+

+
        assert!(Alias::from_str("").is_err());
+
        assert!(Alias::from_str(" ").is_err());
+
        assert!(Alias::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").is_err());
+
        assert!(Alias::from_str("cloud\0head").is_err());
+
        assert!(Alias::from_str("cloud head").is_err());
+
        assert!(Alias::from_str("cloudhead\n").is_err());
+
    }
}
modified radicle/src/node/address/schema.sql
@@ -7,7 +7,7 @@ create table if not exists "nodes" (
  -- Node features.
  "features"           integer   not null,
  -- Node alias.
-
  "alias"              text      default null,
+
  "alias"              text      not null,
  --- Node announcement proof-of-work.
  "pow"                integer   default 0,
  -- Node announcement timestamp.
modified radicle/src/node/address/store.rs
@@ -1,4 +1,5 @@
use std::path::Path;
+
use std::str::FromStr;
use std::{fmt, io};

use localtime::LocalTime;
@@ -7,9 +8,7 @@ use thiserror::Error;

use crate::node;
use crate::node::address::{KnownAddress, Source};
-
use crate::node::Address;
-
use crate::node::AliasStore;
-
use crate::node::NodeId;
+
use crate::node::{Address, Alias, AliasError, AliasStore, NodeId};
use crate::prelude::Timestamp;
use crate::sql::transaction;

@@ -21,6 +20,8 @@ pub enum Error {
    /// I/O error.
    #[error("i/o error: {0}")]
    Io(#[from] io::Error),
+
    #[error("alias error: {0}")]
+
    InvalidAlias(#[from] AliasError),
    /// An Internal error.
    #[error("internal error: {0}")]
    Internal(#[from] sql::Error),
@@ -77,7 +78,7 @@ impl Store for Book {

        if let Some(Ok(row)) = stmt.into_iter().next() {
            let features = row.read::<node::Features, _>("features");
-
            let alias = row.read::<Option<&str>, _>("alias").map(ToOwned::to_owned);
+
            let alias = Alias::from_str(row.read::<&str, _>("alias"))?;
            let timestamp = row.read::<i64, _>("timestamp") as Timestamp;
            let pow = row.read::<i64, _>("pow") as u32;
            let mut addrs = Vec::new();
@@ -130,7 +131,7 @@ impl Store for Book {
        &mut self,
        node: &NodeId,
        features: node::Features,
-
        alias: &str,
+
        alias: Alias,
        pow: u32,
        timestamp: Timestamp,
        addrs: impl IntoIterator<Item = KnownAddress>,
@@ -143,15 +144,10 @@ impl Store for Book {
                 SET features = ?2, alias = ?3, pow = ?4, timestamp = ?5
                 WHERE timestamp < ?5",
            )?;
-
            let alias = if alias.is_empty() {
-
                sql::Value::Null
-
            } else {
-
                sql::Value::String(alias.to_owned())
-
            };

            stmt.bind((1, node))?;
            stmt.bind((2, features))?;
-
            stmt.bind((3, alias))?;
+
            stmt.bind((3, sql::Value::String(alias.into())))?;
            stmt.bind((4, pow as i64))?;
            stmt.bind((5, timestamp as i64))?;
            stmt.next()?;
@@ -263,9 +259,9 @@ impl Store for Book {
impl AliasStore for Book {
    /// Retrieve `alias` of given node.
    /// Calls `Self::get` under the hood.
-
    fn alias(&self, nid: &NodeId) -> Option<String> {
+
    fn alias(&self, nid: &NodeId) -> Option<Alias> {
        self.get(nid)
-
            .map(|node| node.and_then(|n| n.alias))
+
            .map(|node| node.map(|n| n.alias))
            .unwrap_or(None)
    }
}
@@ -283,7 +279,7 @@ pub trait Store {
        &mut self,
        node: &NodeId,
        features: node::Features,
-
        alias: &str,
+
        alias: Alias,
        pow: u32,
        timestamp: Timestamp,
        addrs: impl IntoIterator<Item = KnownAddress>,
@@ -409,16 +405,16 @@ mod test {
        let timestamp = LocalTime::now().as_millis();

        cache
-
            .insert(&alice, features, "alice", 16, timestamp, [])
+
            .insert(&alice, features, Alias::new("alice"), 16, timestamp, [])
            .unwrap();
        let node = cache.get(&alice).unwrap().unwrap();
-
        assert_eq!(node.alias.as_deref(), Some("alice"));
+
        assert_eq!(node.alias.as_ref(), "alice");

        cache
-
            .insert(&alice, features, "", 16, timestamp + 1, [])
+
            .insert(&alice, features, Alias::new("bob"), 16, timestamp + 1, [])
            .unwrap();
        let node = cache.get(&alice).unwrap().unwrap();
-
        assert_eq!(node.alias.as_deref(), None);
+
        assert_eq!(node.alias.as_ref(), "bob");
    }

    #[test]
@@ -435,7 +431,14 @@ mod test {
            last_attempt: None,
        };
        let inserted = cache
-
            .insert(&alice, features, "alice", 16, timestamp, [ka.clone()])
+
            .insert(
+
                &alice,
+
                features,
+
                Alias::new("alice"),
+
                16,
+
                timestamp,
+
                [ka.clone()],
+
            )
            .unwrap();
        assert!(inserted);

@@ -444,7 +447,7 @@ mod test {
        assert_eq!(node.features, features);
        assert_eq!(node.pow, 16);
        assert_eq!(node.timestamp, timestamp);
-
        assert_eq!(node.alias.as_deref(), Some("alice"));
+
        assert_eq!(node.alias.as_ref(), "alice");
        assert_eq!(node.addrs, vec![ka]);
    }

@@ -454,6 +457,7 @@ mod test {
        let mut cache = Book::memory().unwrap();
        let features = node::Features::SEED;
        let timestamp = LocalTime::now().as_millis();
+
        let alias = Alias::new("alice");

        let ka = KnownAddress {
            addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
@@ -462,12 +466,12 @@ mod test {
            last_attempt: None,
        };
        let inserted = cache
-
            .insert(&alice, features, "alice", 0, timestamp, [ka.clone()])
+
            .insert(&alice, features, alias.clone(), 0, timestamp, [ka.clone()])
            .unwrap();
        assert!(inserted);

        let inserted = cache
-
            .insert(&alice, features, "alice", 0, timestamp, [ka])
+
            .insert(&alice, features, alias, 0, timestamp, [ka])
            .unwrap();
        assert!(!inserted);

@@ -480,6 +484,8 @@ mod test {
        let mut cache = Book::memory().unwrap();
        let timestamp = LocalTime::now().as_millis();
        let features = node::Features::SEED;
+
        let alias1 = Alias::new("alice");
+
        let alias2 = Alias::new("~alice~");
        let ka = KnownAddress {
            addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
            source: Source::Peer,
@@ -488,45 +494,38 @@ mod test {
        };

        let updated = cache
-
            .insert(&alice, features, "alice", 0, timestamp, [ka.clone()])
+
            .insert(&alice, features, alias1, 0, timestamp, [ka.clone()])
            .unwrap();
        assert!(updated);

        let updated = cache
-
            .insert(&alice, features, "~alice~", 0, timestamp, [])
+
            .insert(&alice, features, alias2.clone(), 0, timestamp, [])
            .unwrap();
        assert!(!updated, "Can't update using the same timestamp");

        let updated = cache
-
            .insert(&alice, features, "~alice~", 0, timestamp - 1, [])
+
            .insert(&alice, features, alias2.clone(), 0, timestamp - 1, [])
            .unwrap();
        assert!(!updated, "Can't update using a smaller timestamp");

        let node = cache.get(&alice).unwrap().unwrap();
-
        assert_eq!(node.alias.as_deref(), Some("alice"));
+
        assert_eq!(node.alias.as_ref(), "alice");
        assert_eq!(node.timestamp, timestamp);
        assert_eq!(node.pow, 0);

        let updated = cache
-
            .insert(&alice, features, "~alice~", 0, timestamp + 1, [])
+
            .insert(&alice, features, alias2.clone(), 0, timestamp + 1, [])
            .unwrap();
        assert!(updated, "Can update with a larger timestamp");

        let updated = cache
-
            .insert(
-
                &alice,
-
                node::Features::NONE,
-
                "~alice~",
-
                1,
-
                timestamp + 2,
-
                [],
-
            )
+
            .insert(&alice, node::Features::NONE, alias2, 1, timestamp + 2, [])
            .unwrap();
        assert!(updated);

        let node = cache.get(&alice).unwrap().unwrap();
        assert_eq!(node.features, node::Features::NONE);
-
        assert_eq!(node.alias.as_deref(), Some("~alice~"));
+
        assert_eq!(node.alias.as_ref(), "~alice~");
        assert_eq!(node.timestamp, timestamp + 2);
        assert_eq!(node.pow, 1);
        assert_eq!(node.addrs, vec![ka]);
@@ -539,6 +538,8 @@ mod test {
        let mut cache = Book::memory().unwrap();
        let timestamp = LocalTime::now().as_millis();
        let features = node::Features::SEED;
+
        let alice_alias = Alias::new("alice");
+
        let bob_alias = Alias::new("bob");

        for addr in [
            ([4, 4, 4, 4], 8776),
@@ -552,10 +553,17 @@ mod test {
                last_attempt: None,
            };
            cache
-
                .insert(&alice, features, "alice", 0, timestamp, [ka.clone()])
+
                .insert(
+
                    &alice,
+
                    features,
+
                    alice_alias.clone(),
+
                    0,
+
                    timestamp,
+
                    [ka.clone()],
+
                )
                .unwrap();
            cache
-
                .insert(&bob, features, "bob", 0, timestamp, [ka])
+
                .insert(&bob, features, bob_alias.clone(), 0, timestamp, [ka])
                .unwrap();
        }
        assert_eq!(cache.len().unwrap(), 6);
@@ -577,6 +585,7 @@ mod test {
        let mut expected = Vec::new();
        let timestamp = LocalTime::now().as_millis();
        let features = node::Features::SEED;
+
        let alias = Alias::new("alice");

        for id in ids {
            let ip = rng.u32(..);
@@ -590,7 +599,7 @@ mod test {
            };
            expected.push((id, ka.clone()));
            cache
-
                .insert(&id, features, "alias", 0, timestamp, [ka])
+
                .insert(&id, features, alias.clone(), 0, timestamp, [ka])
                .unwrap();
        }

modified radicle/src/node/address/types.rs
@@ -5,7 +5,7 @@ use nonempty::NonEmpty;

use crate::collections::HashMap;
use crate::node;
-
use crate::node::Address;
+
use crate::node::{Address, Alias};
use crate::prelude::Timestamp;

/// A map with the ability to randomly select values.
@@ -78,7 +78,7 @@ impl<K, V> DerefMut for AddressBook<K, V> {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Node {
    /// Advertized alias.
-
    pub alias: Option<String>,
+
    pub alias: Alias,
    /// Advertized features.
    pub features: node::Features,
    /// Advertized addresses
added radicle/src/node/config.rs
@@ -0,0 +1,126 @@
+
use std::ops::Deref;
+

+
use cyphernet::addr::PeerAddr;
+
use localtime::LocalDuration;
+

+
use crate::node;
+
use crate::node::tracking::{Policy, Scope};
+
use crate::node::{Address, Alias, NodeId};
+

+
/// Peer-to-peer network.
+
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
pub enum Network {
+
    #[default]
+
    Main,
+
    Test,
+
}
+

+
/// Configuration parameters defining attributes of minima and maxima.
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
pub struct Limits {
+
    /// Number of routing table entries before we start pruning.
+
    pub routing_max_size: usize,
+
    /// How long to keep a routing table entry before being pruned.
+
    #[serde(with = "crate::serde_ext::localtime::duration")]
+
    pub routing_max_age: LocalDuration,
+
    /// Maximum number of concurrent fetches per per connection.
+
    pub fetch_concurrency: usize,
+
}
+

+
impl Default for Limits {
+
    fn default() -> Self {
+
        Self {
+
            routing_max_size: 1000,
+
            routing_max_age: LocalDuration::from_mins(7 * 24 * 60),
+
            fetch_concurrency: 1,
+
        }
+
    }
+
}
+

+
/// Full address used to connect to a remote node.
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+
#[serde(transparent)]
+
pub struct ConnectAddress(#[serde(with = "crate::serde_ext::string")] PeerAddr<NodeId, Address>);
+

+
impl From<PeerAddr<NodeId, Address>> for ConnectAddress {
+
    fn from(value: PeerAddr<NodeId, Address>) -> Self {
+
        Self(value)
+
    }
+
}
+

+
impl From<ConnectAddress> for (NodeId, Address) {
+
    fn from(value: ConnectAddress) -> Self {
+
        (value.0.id, value.0.addr)
+
    }
+
}
+

+
impl From<(NodeId, Address)> for ConnectAddress {
+
    fn from((id, addr): (NodeId, Address)) -> Self {
+
        Self(PeerAddr { id, addr })
+
    }
+
}
+

+
impl Deref for ConnectAddress {
+
    type Target = PeerAddr<NodeId, Address>;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
/// Service configuration.
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
pub struct Config {
+
    /// Node alias.
+
    pub alias: Alias,
+
    /// Peers to connect to on startup.
+
    /// Connections to these peers will be maintained.
+
    pub connect: Vec<ConnectAddress>,
+
    /// Specify the node's public addresses
+
    pub external_addresses: Vec<Address>,
+
    /// Peer-to-peer network.
+
    pub network: Network,
+
    /// Whether or not our node should relay inventories.
+
    pub relay: bool,
+
    /// Configured service limits.
+
    pub limits: Limits,
+
    /// Default tracking policy.
+
    pub policy: Policy,
+
    /// Default tracking scope.
+
    pub scope: Scope,
+
}
+

+
impl Config {
+
    pub fn new(alias: Alias) -> Self {
+
        Self {
+
            alias,
+
            connect: Vec::default(),
+
            external_addresses: vec![],
+
            network: Network::default(),
+
            relay: true,
+
            limits: Limits::default(),
+
            policy: Policy::default(),
+
            scope: Scope::default(),
+
        }
+
    }
+
}
+

+
impl Config {
+
    pub fn peer(&self, id: &NodeId) -> Option<&Address> {
+
        self.connect
+
            .iter()
+
            .find(|ca| &ca.id == id)
+
            .map(|ca| &ca.addr)
+
    }
+

+
    pub fn is_persistent(&self, id: &NodeId) -> bool {
+
        self.peer(id).is_some()
+
    }
+

+
    pub fn features(&self) -> node::Features {
+
        node::Features::SEED
+
    }
+
}
modified radicle/src/node/tracking.rs
@@ -8,7 +8,7 @@ use thiserror::Error;

use crate::prelude::Id;

-
use super::NodeId;
+
pub use super::{Alias, NodeId};

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Repo {
@@ -24,11 +24,9 @@ pub struct Node {
    pub policy: Policy,
}

-
/// Node alias.
-
pub type Alias = String;
-

/// Tracking policy.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
pub enum Policy {
    /// The resource is tracked.
    Track,
@@ -91,6 +89,7 @@ impl TryFrom<&sqlite::Value> for Policy {

/// Tracking scope of a repository tracking policy.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
pub enum Scope {
    /// Track remotes of nodes that are already tracked.
    #[default]
modified radicle/src/node/tracking/store.rs
@@ -1,11 +1,11 @@
#![allow(clippy::type_complexity)]
use std::path::Path;
-
use std::{fmt, io, ops::Not as _, time};
+
use std::{fmt, io, ops::Not as _, str::FromStr, time};

use sqlite as sql;
use thiserror::Error;

-
use crate::node::AliasStore;
+
use crate::node::{Alias, AliasStore};
use crate::prelude::{Id, NodeId};

use super::{Node, Policy, Repo, Scope};
@@ -188,7 +188,11 @@ impl Config {

        if let Some(Ok(row)) = stmt.into_iter().next() {
            let alias = row.read::<&str, _>("alias");
-
            let alias = alias.is_empty().not().then_some(alias.to_owned());
+
            let alias = alias
+
                .is_empty()
+
                .not()
+
                .then_some(alias.to_owned())
+
                .and_then(|s| Alias::from_str(&s).ok());
            let policy = row.read::<Policy, _>("policy");

            return Ok(Some(Node {
@@ -229,7 +233,11 @@ impl Config {
        while let Some(Ok(row)) = stmt.next() {
            let id = row.read("id");
            let alias = row.read::<&str, _>("alias").to_owned();
-
            let alias = alias.is_empty().not().then_some(alias.to_owned());
+
            let alias = alias
+
                .is_empty()
+
                .not()
+
                .then_some(alias.to_owned())
+
                .and_then(|s| Alias::from_str(&s).ok());
            let policy = row.read::<Policy, _>("policy");

            entries.push(Node { id, alias, policy });
@@ -260,7 +268,7 @@ impl Config {
impl AliasStore for Config {
    /// Retrieve `alias` of given node.
    /// Calls `Self::node_policy` under the hood.
-
    fn alias(&self, nid: &NodeId) -> Option<String> {
+
    fn alias(&self, nid: &NodeId) -> Option<Alias> {
        self.node_policy(nid)
            .map(|node| node.and_then(|n| n.alias))
            .unwrap_or(None)
@@ -334,7 +342,7 @@ mod test {
        assert!(db.track_node(&id, Some("eve")).unwrap());
        assert_eq!(
            db.node_policy(&id).unwrap().unwrap().alias,
-
            Some(String::from("eve"))
+
            Some(Alias::from_str("eve").unwrap())
        );
        assert!(db.track_node(&id, None).unwrap());
        assert_eq!(db.node_policy(&id).unwrap().unwrap().alias, None);
@@ -342,7 +350,7 @@ mod test {
        assert!(db.track_node(&id, Some("alice")).unwrap());
        assert_eq!(
            db.node_policy(&id).unwrap().unwrap().alias,
-
            Some(String::from("alice"))
+
            Some(Alias::new("alice"))
        );
    }

modified radicle/src/profile.rs
@@ -10,16 +10,18 @@
//!     node/
//!       control.sock                           # Node control socket
//!
+
use std::io::Write;
use std::path::{Path, PathBuf};
-
use std::{fs, io};
+
use std::{fs, io, str::FromStr};

+
use serde::Serialize;
use thiserror::Error;

use crate::crypto::ssh::agent::Agent;
use crate::crypto::ssh::{keystore, Keystore, Passphrase};
use crate::crypto::{PublicKey, Signer};
use crate::node;
-
use crate::node::{address, routing, tracking, AliasStore};
+
use crate::node::{address, routing, tracking, Alias, AliasStore};
use crate::prelude::Did;
use crate::prelude::NodeId;
use crate::storage::git::transport;
@@ -49,10 +51,12 @@ pub enum Error {
    #[error(transparent)]
    Io(#[from] io::Error),
    #[error(transparent)]
+
    Config(#[from] ConfigError),
+
    #[error(transparent)]
    Keystore(#[from] keystore::Error),
    #[error(transparent)]
    MemorySigner(#[from] keystore::MemorySignerError),
-
    #[error("no profile found at the filepath '{0}'")]
+
    #[error("no profile found at path '{0}'")]
    NotFound(PathBuf),
    #[error("error connecting to ssh-agent: {0}")]
    Agent(#[from] crate::crypto::ssh::agent::Error),
@@ -64,19 +68,86 @@ pub enum Error {
    AddressStore(#[from] node::address::Error),
}

+
#[derive(Debug, Error)]
+
pub enum ConfigError {
+
    #[error("failed to load node configuration from {0}: {1}")]
+
    Io(PathBuf, io::Error),
+
    #[error("failed to decode node configuration from {0}: {1}")]
+
    Json(PathBuf, serde_json::Error),
+
}
+

+
/// Local radicle configuration.
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
pub struct Config {
+
    pub node: node::Config,
+
}
+

+
impl Config {
+
    /// Initialize a new configuration. Fails if the path already exists.
+
    pub fn init(alias: Alias, path: &Path) -> io::Result<Self> {
+
        let cfg = Self {
+
            node: node::Config::new(alias),
+
        };
+
        let mut file = fs::OpenOptions::new()
+
            .create_new(true)
+
            .write(true)
+
            .open(path)?;
+
        let formatter = serde_json::ser::PrettyFormatter::with_indent(b"  ");
+
        let mut serializer = serde_json::Serializer::with_formatter(&file, formatter);
+

+
        cfg.serialize(&mut serializer)?;
+
        file.write_all(b"\n")?;
+
        file.sync_all()?;
+

+
        Ok(cfg)
+
    }
+

+
    /// Load a configuration from the given path.
+
    pub fn load(path: &Path) -> Result<Self, ConfigError> {
+
        match fs::File::open(path) {
+
            Ok(cfg) => {
+
                serde_json::from_reader(cfg).map_err(|e| ConfigError::Json(path.to_path_buf(), e))
+
            }
+
            Err(e) => {
+
                let Ok(user) = env::var("USER") else {
+
                    return Err(ConfigError::Io(path.to_owned(), e));
+
                };
+
                let Ok(alias) = Alias::from_str(&user) else {
+
                    return Err(ConfigError::Io(path.to_owned(), e));
+
                };
+
                Ok(Config {
+
                    node: node::Config::new(alias),
+
                })
+
            }
+
        }
+
    }
+

+
    /// Get the user alias.
+
    pub fn alias(&self) -> &Alias {
+
        &self.node.alias
+
    }
+
}
+

#[derive(Debug, Clone)]
pub struct Profile {
    pub home: Home,
    pub storage: Storage,
    pub keystore: Keystore,
    pub public_key: PublicKey,
+
    pub config: Config,
}

impl Profile {
-
    pub fn init(home: Home, passphrase: impl Into<Passphrase>) -> Result<Self, Error> {
+
    pub fn init(
+
        home: Home,
+
        alias: Alias,
+
        passphrase: impl Into<Passphrase>,
+
    ) -> Result<Self, Error> {
        let storage = Storage::open(home.storage())?;
        let keystore = Keystore::new(&home.keys());
        let public_key = keystore.init("radicle", passphrase)?;
+
        let config = Config::init(alias, home.config().as_path())?;

        transport::local::register(storage.clone());

@@ -85,6 +156,7 @@ impl Profile {
            storage,
            keystore,
            public_key,
+
            config,
        })
    }

@@ -95,6 +167,7 @@ impl Profile {
        let public_key = keystore
            .public_key()?
            .ok_or_else(|| Error::NotFound(home.path().to_path_buf()))?;
+
        let config = Config::load(home.config().as_path())?;

        transport::local::register(storage.clone());

@@ -103,6 +176,7 @@ impl Profile {
            storage,
            keystore,
            public_key,
+
            config,
        })
    }

@@ -194,7 +268,7 @@ pub struct Aliases {
impl AliasStore for Aliases {
    /// Retrieve `alias` of given node.
    /// First looks in `tracking.db` and then `addresses.db`.
-
    fn alias(&self, nid: &NodeId) -> Option<String> {
+
    fn alias(&self, nid: &NodeId) -> Option<Alias> {
        self.tracking
            .alias(nid)
            .or_else(|| self.addresses.alias(nid))
@@ -267,6 +341,10 @@ impl Home {
        self.path.join("storage")
    }

+
    pub fn config(&self) -> PathBuf {
+
        self.path.join("config.json")
+
    }
+

    pub fn keys(&self) -> PathBuf {
        self.path.join("keys")
    }
modified radicle/src/serde_ext.rs
@@ -24,28 +24,53 @@ pub mod string {
    }
}

-
/// Unlike the default `serde` instance for `LocalTime`, this encodes and decodes using seconds
+
/// Unlike the default `serde` instances from `localtime`, this encodes and decodes using seconds
/// instead of milliseconds.
pub mod localtime {
-
    use localtime::LocalTime;
-
    use serde::{de, Deserialize, Deserializer, Serializer};
+
    pub mod time {
+
        use localtime::LocalTime;
+
        use serde::{de, Deserialize, Deserializer, Serializer};

-
    pub fn serialize<S>(value: &LocalTime, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        serializer.collect_str(&value.as_secs())
+
        pub fn serialize<S>(value: &LocalTime, serializer: S) -> Result<S::Ok, S::Error>
+
        where
+
            S: Serializer,
+
        {
+
            serializer.collect_str(&value.as_secs())
+
        }
+

+
        pub fn deserialize<'de, D>(deserializer: D) -> Result<LocalTime, D::Error>
+
        where
+
            D: Deserializer<'de>,
+
        {
+
            let seconds: u64 = String::deserialize(deserializer)?
+
                .parse()
+
                .map_err(de::Error::custom)?;
+

+
            Ok(LocalTime::from_secs(seconds))
+
        }
    }

-
    pub fn deserialize<'de, D>(deserializer: D) -> Result<LocalTime, D::Error>
-
    where
-
        D: Deserializer<'de>,
-
    {
-
        let seconds: u64 = String::deserialize(deserializer)?
-
            .parse()
-
            .map_err(de::Error::custom)?;
+
    pub mod duration {
+
        use localtime::LocalDuration;
+
        use serde::{de, Deserialize, Deserializer, Serializer};
+

+
        pub fn serialize<S>(value: &LocalDuration, serializer: S) -> Result<S::Ok, S::Error>
+
        where
+
            S: Serializer,
+
        {
+
            serializer.collect_str(&value.as_secs())
+
        }
+

+
        pub fn deserialize<'de, D>(deserializer: D) -> Result<LocalDuration, D::Error>
+
        where
+
            D: Deserializer<'de>,
+
        {
+
            let seconds: u64 = String::deserialize(deserializer)?
+
                .parse()
+
                .map_err(de::Error::custom)?;

-
        Ok(LocalTime::from_secs(seconds))
+
            Ok(LocalDuration::from_secs(seconds))
+
        }
    }
}

modified radicle/src/test/arbitrary.rs
@@ -1,6 +1,7 @@
use std::collections::{BTreeMap, HashSet};
use std::hash::Hash;
use std::ops::RangeBounds;
+
use std::str::FromStr;
use std::{iter, net};

use crypto::test::signer::MockSigner;
@@ -14,7 +15,7 @@ use crate::identity::{
    project::Project,
    Did,
};
-
use crate::node::Address;
+
use crate::node::{Address, Alias};
use crate::storage;
use crate::storage::refs::{Refs, SignedRefs};
use crate::test::storage::{MockRepository, MockStorage};
@@ -227,3 +228,13 @@ impl Arbitrary for Address {
        })
    }
}
+

+
impl Arbitrary for Alias {
+
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
+
        let s = g
+
            .choose(&["cloudhead", "alice", "bob", "john-lu", "f0_"])
+
            .unwrap();
+

+
        Alias::from_str(s).unwrap()
+
    }
+
}