Member tokens: reusable account credential authorizing key enrollment per node

This commit is contained in:
George Coles
2026-09-15 12:34:14 -04:00
parent 9aa0117af4
commit 98ff17002f
6 changed files with 172 additions and 47 deletions
+41 -13
View File
@@ -472,10 +472,11 @@ pub fn registry_approve(dir: &Path, id: &str, registry_url: Option<&str>) -> Res
});
Ok(())
})?;
let invite = registry::create_invite(dir, id, 24 * 3600)?;
let token = registry::create_token(dir, id)?;
let (_, signed) = open_registry(dir)?;
println!("approved {id} ({class})");
print_credential_block(id, &invite.token, registry_url, &signed.doc.ma_key);
println!("member token — reusable for every node the member runs; keep private:");
print_credential_block(id, &token, registry_url, &signed.doc.ma_key);
Ok(())
}
@@ -487,13 +488,33 @@ pub fn registry_invite(dir: &Path, id: &str, registry_url: Option<&str>) -> Resu
return Err(anyhow!("no member named {id}"));
}
let invite = registry::create_invite(dir, id, 24 * 3600)?;
println!("single-use handoff invite for {id} (valid 24h):");
print_credential_block(id, &invite.token, registry_url, &signed.doc.ma_key);
Ok(())
}
/// Mints an additional member token (account credential) and prints the block.
/// Returns the raw token for programmatic use.
pub fn registry_token(dir: &Path, id: &str, registry_url: Option<&str>) -> Result<String> {
let (_, signed) = open_registry(dir)?;
if !signed.doc.members.iter().any(|member| member.id == id) {
return Err(anyhow!("no member named {id}"));
}
let token = registry::create_token(dir, id)?;
println!("member token for {id} — reusable for every node they run; keep private:");
print_credential_block(id, &token, registry_url, &signed.doc.ma_key);
Ok(token)
}
/// Revokes all of a member's tokens (e.g. after a leak); mint fresh with `registry token`.
pub fn registry_revoke_token(dir: &Path, id: &str) -> Result<()> {
let revoked = registry::revoke_tokens(dir, id)?;
println!("revoked {revoked} token(s) for {id}");
Ok(())
}
fn print_credential_block(id: &str, token: &str, registry_url: Option<&str>, ma_key: &str) {
let registry_url = registry_url.unwrap_or("<registry-url>");
println!("hand this credential block to the member (single use, valid 24h):");
println!("id={id} token={token} registry={registry_url} ma_key={ma_key}");
}
@@ -741,12 +762,16 @@ async fn registry_enroll(
.into_response();
}
};
if let Err(error) = registry::redeem_invite(&server.dir, &request.id, &request.token) {
return (
StatusCode::FORBIDDEN,
Json(serde_json::json!({ "error": error.to_string() })),
)
.into_response();
if registry::redeem_invite(&server.dir, &request.id, &request.token).is_err() {
let valid = registry::validate_token(&server.dir, &request.id, &request.token)
.unwrap_or(false);
if !valid {
return (
StatusCode::FORBIDDEN,
Json(serde_json::json!({ "error": "unknown invite or member token" })),
)
.into_response();
}
}
let result = mutate_registry(&server.dir, |doc| {
let Some(member) = doc
@@ -872,10 +897,10 @@ the MA for the membership contract — never published, never on the wire.</p>
<h2>2. Download</h2>
<div class="card">
<p>Static Linux x86_64 binaries (musl — no runtime dependencies):</p>
<pre class="cmd">curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.3/frxd-linux-amd64
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.3/frxd-linux-amd64.sha256
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.3/frx-linux-amd64
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.3/frx-linux-amd64.sha256</pre>
<pre class="cmd">curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.4/frxd-linux-amd64
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.4/frxd-linux-amd64.sha256
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.4/frx-linux-amd64
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.4/frx-linux-amd64.sha256</pre>
<p class="muted">All releases: <a href="https://git.federatedsearch.org/frx/frxd/releases">git.federatedsearch.org/frx/frxd/releases</a>.
Source and spec (<code>rfc.txt</code>): <a href="https://git.federatedsearch.org/frx/frxd">git.federatedsearch.org/frx/frxd</a>.</p>
</div>
@@ -899,6 +924,9 @@ against the pinned MA key, and wires the federation relays — no domains, DNS,
needed on your side.</li>
<li>Index a directory and mark what you share:</li>
</ol>
<p class="muted">The credential block is reusable: run the wizard on every node you operate —
each node binds its own key to your identifier. If the token leaks, the MA revokes it and
issues a fresh one.</p>
<pre class="cmd">frxd add ~/documents --name docs --shared --exposure metadata
frxd serve</pre>
<p class="muted">Search local-first with <code>frx search "..."</code>; broadcast to the federation with