Signup without a code queues a pending application; registry approve issues credentials
This commit is contained in:
+144
-47
@@ -431,7 +431,7 @@ pub fn registry_applications(dir: &Path) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
for app in &applications {
|
||||
println!("{} [{}] {} <{}>", app.id, app.class, app.org, app.email);
|
||||
println!("{} [{}] {} <{}> — {}", app.id, app.class, app.org, app.email, app.status);
|
||||
println!(" representative: {}", app.representative);
|
||||
if !app.address.is_empty() {
|
||||
println!(" address: {}", app.address);
|
||||
@@ -449,6 +449,41 @@ pub fn registry_applications(dir: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Approves a pending application: creates the member stub (class from the application),
|
||||
/// issues a 24h invite, and prints the credential block to hand to the member.
|
||||
pub fn registry_approve(dir: &Path, id: &str, registry_url: Option<&str>) -> Result<()> {
|
||||
let (_, signed) = open_registry(dir)?;
|
||||
if signed.doc.members.iter().any(|member| member.id == id) {
|
||||
return Err(anyhow!("member {id} already listed"));
|
||||
}
|
||||
let application = registry::approve_application(dir, id)?;
|
||||
let class = if application.class == CLASS_ENRICHMENT {
|
||||
CLASS_ENRICHMENT
|
||||
} else {
|
||||
CLASS_SOURCE
|
||||
}
|
||||
.to_string();
|
||||
mutate_registry(dir, |doc| {
|
||||
doc.members.push(RegistryMember {
|
||||
id: id.to_string(),
|
||||
class: class.clone(),
|
||||
keys: Vec::new(),
|
||||
enc_key: None,
|
||||
});
|
||||
Ok(())
|
||||
})?;
|
||||
let invite = registry::create_invite(dir, id, 24 * 3600)?;
|
||||
let (_, signed) = open_registry(dir)?;
|
||||
let registry_url = registry_url.unwrap_or("<registry-url>");
|
||||
println!("approved {id} ({class})");
|
||||
println!("hand this credential block to the member:");
|
||||
println!(
|
||||
"id={id} token={} registry={registry_url} ma_key={}",
|
||||
invite.token, signed.doc.ma_key
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn registry_set_relays(dir: &Path, relays: &[String]) -> Result<()> {
|
||||
mutate_registry(dir, |doc| {
|
||||
doc.relays = relays.to_vec();
|
||||
@@ -570,20 +605,36 @@ async fn registry_signup(
|
||||
State(server): State<std::sync::Arc<RegistryServer>>,
|
||||
Json(request): Json<SignupRequest>,
|
||||
) -> Response {
|
||||
let Some(expected) = &server.signup_code else {
|
||||
let label = sanitize_label(&request.label);
|
||||
if label.is_empty() {
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(serde_json::json!({ "error": "signup is not enabled" })),
|
||||
)
|
||||
.into_response();
|
||||
};
|
||||
if request.code.as_deref() != Some(expected.as_str()) {
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(serde_json::json!({ "error": "wrong signup code" })),
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "label must be alphanumeric" })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
// A pre-approval code, if supplied, must be valid; an empty code queues for review.
|
||||
let code = request
|
||||
.code
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|code| !code.is_empty());
|
||||
if let Some(code) = code {
|
||||
let Some(expected) = &server.signup_code else {
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(serde_json::json!({ "error": "no pre-approval code is configured; submit without one to queue for review" })),
|
||||
)
|
||||
.into_response();
|
||||
};
|
||||
if code != expected.as_str() {
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(serde_json::json!({ "error": "wrong signup code" })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
if !request.attestation {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
@@ -598,11 +649,13 @@ async fn registry_signup(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let label = sanitize_label(&request.label);
|
||||
if label.is_empty() {
|
||||
if request.org.trim().is_empty()
|
||||
|| request.representative.trim().is_empty()
|
||||
|| request.email.trim().is_empty()
|
||||
{
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "label must be alphanumeric" })),
|
||||
Json(serde_json::json!({ "error": "organization name, representative, and contact email are required" })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
@@ -625,8 +678,9 @@ async fn registry_signup(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let invite = match registry::create_invite(&server.dir, &id, 24 * 3600) {
|
||||
Ok(invite) => invite,
|
||||
let applications = match registry::load_applications(®istry::applications_path(&server.dir))
|
||||
{
|
||||
Ok(applications) => applications,
|
||||
Err(error) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -635,26 +689,18 @@ async fn registry_signup(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
if applications.iter().any(|application| application.id == id) {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
Json(serde_json::json!({ "error": "an application for this identifier is already on file" })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let class = if request.class.as_deref() == Some(CLASS_ENRICHMENT) {
|
||||
CLASS_ENRICHMENT
|
||||
} else {
|
||||
CLASS_SOURCE
|
||||
};
|
||||
if let Err(error) = mutate_registry(&server.dir, |doc| {
|
||||
doc.members.push(RegistryMember {
|
||||
id: id.clone(),
|
||||
class: class.to_string(),
|
||||
keys: Vec::new(),
|
||||
enc_key: None,
|
||||
});
|
||||
Ok(())
|
||||
}) {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": error.to_string() })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let application = registry::Application {
|
||||
id: id.clone(),
|
||||
org: request.org.clone(),
|
||||
@@ -665,8 +711,31 @@ async fn registry_signup(
|
||||
class: class.to_string(),
|
||||
payment: request.payment.clone(),
|
||||
privacy_link: request.privacy_link.clone(),
|
||||
status: if code.is_some() {
|
||||
"approved".to_string()
|
||||
} else {
|
||||
"pending".to_string()
|
||||
},
|
||||
submitted_at: now_ts(),
|
||||
};
|
||||
if code.is_none() {
|
||||
if let Err(error) = registry::record_application(&server.dir, application) {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": error.to_string() })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
return (
|
||||
StatusCode::ACCEPTED,
|
||||
Json(serde_json::json!({
|
||||
"status": "pending",
|
||||
"id": id,
|
||||
"message": "application received — the membership authority reviews it and issues your credential block"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if let Err(error) = registry::record_application(&server.dir, application) {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -674,6 +743,31 @@ async fn registry_signup(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let invite = match registry::create_invite(&server.dir, &id, 24 * 3600) {
|
||||
Ok(invite) => invite,
|
||||
Err(error) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": error.to_string() })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
if let Err(error) = mutate_registry(&server.dir, |doc| {
|
||||
doc.members.push(RegistryMember {
|
||||
id: id.clone(),
|
||||
class: class.to_string(),
|
||||
keys: Vec::new(),
|
||||
enc_key: None,
|
||||
});
|
||||
Ok(())
|
||||
}) {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": error.to_string() })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let registry_url = server.registry_url.clone().unwrap_or_default();
|
||||
let ma_key = signed.doc.ma_key.clone();
|
||||
(
|
||||
@@ -816,20 +910,21 @@ scores on the wire, no in-protocol payment.</p>
|
||||
|
||||
<h2>1. Register with the membership authority</h2>
|
||||
<div class="card">
|
||||
<p class="muted">Registering here creates your identifier with the membership authority (MA); the
|
||||
onboarding wizard in step 4 then binds your node's keys to it. The public registry publishes only
|
||||
your identifier, class, keys, and the federation's relays. The organization details below are kept
|
||||
privately by the MA for the membership contract — they are never published and never travel on the
|
||||
wire.</p>
|
||||
<p class="muted">This form requests membership from the membership authority (MA). The MA reviews
|
||||
your organization details and issues a credential block
|
||||
(<code>id=... token=... registry=... ma_key=...</code>); the onboarding wizard in step 4 then
|
||||
binds your node's keys to the identifier. The public registry publishes only your identifier,
|
||||
class, keys, and the federation's relays. The organization details below are kept privately by
|
||||
the MA for the membership contract — never published, never on the wire.</p>
|
||||
<form id="f">
|
||||
<label>Short name — this becomes your identifier<br>
|
||||
<input name="label" id="label" required pattern="[A-Za-z0-9 -]+" placeholder="keswick-research"></label>
|
||||
<p class="muted">identifier: <code id="preview">(type a short name)</code> — no domain or DNS of your own is needed.</p>
|
||||
<label>Signup code<br>
|
||||
<input name="code" type="password" placeholder="invite code"></label>
|
||||
<p class="muted">The code is the admission gate: members are approved, not anonymous. Ask the MA
|
||||
operator for one. (If you run the MA, it is the <code>--signup-code</code> passed to
|
||||
<code>frxd registry serve</code>.)</p>
|
||||
<label>Signup code (optional)<br>
|
||||
<input name="code" type="password" placeholder="pre-approval code"></label>
|
||||
<p class="muted">Leave empty to queue your application for MA review — the MA will contact you
|
||||
with your credential block. A pre-approval code, issued by the MA out of band, approves you
|
||||
immediately and returns the block here.</p>
|
||||
<label>Legal organization name<br>
|
||||
<input name="org" required placeholder="Keswick Research LLC"></label>
|
||||
<label>Representative (authorized contact person)<br>
|
||||
@@ -859,10 +954,10 @@ operator for one. (If you run the MA, it is the <code>--signup-code</code> passe
|
||||
<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.1/frxd-linux-amd64
|
||||
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.1/frxd-linux-amd64.sha256
|
||||
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.1/frx-linux-amd64
|
||||
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.1/frx-linux-amd64.sha256</pre>
|
||||
<pre class="cmd">curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.2/frxd-linux-amd64
|
||||
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.2/frxd-linux-amd64.sha256
|
||||
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.2/frx-linux-amd64
|
||||
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.2/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>
|
||||
@@ -931,9 +1026,11 @@ f.onsubmit = async (e) => {
|
||||
});
|
||||
const body = await res.json();
|
||||
out.style.display = "block";
|
||||
out.textContent = res.ok
|
||||
out.textContent = body.credentials
|
||||
? "Membership approved.\n\nNext: download frxd (step 2), install it (step 3), then run `frxd --onboarding` and paste this block:\n\n" + body.credentials + "\n"
|
||||
: "Failed: " + (body.error || ("http " + res.status));
|
||||
: res.ok
|
||||
? "Application received.\n\n" + (body.message || "The membership authority will review it and issue your credential block.")
|
||||
: "Failed: " + (body.error || ("http " + res.status));
|
||||
};
|
||||
|
||||
(async () => {
|
||||
|
||||
Reference in New Issue
Block a user