Onboarding: --listen override and auto-pick free port; live TLS demo verified
This commit is contained in:
+79
-12
@@ -692,29 +692,96 @@ async fn registry_page() -> Response {
|
|||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
const REGISTRY_PAGE: &str = r#"<!doctype html>
|
const REGISTRY_PAGE: &str = r##"<!doctype html>
|
||||||
<html><head><meta charset="utf-8"><title>FRX membership</title></head>
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>FRX — Federated Retrieval Exchange</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; line-height: 1.55;
|
||||||
|
max-width: 44rem; margin: 0 auto; padding: 2.5rem 1rem; color: #1c1c1e; background: #f7f7f9; }
|
||||||
|
h1 { font-size: 1.7rem; margin-bottom: 0.2rem; }
|
||||||
|
h2 { font-size: 1.05rem; margin-top: 2.2rem; }
|
||||||
|
.tag { color: #555; margin-top: 0; }
|
||||||
|
.card { background: #fff; border: 1px solid #ddd; border-radius: 10px; padding: 1.1rem 1.25rem; }
|
||||||
|
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.92em; }
|
||||||
|
input, button { font: inherit; border: 1px solid #bbb; border-radius: 6px; padding: 0.45rem 0.6rem; }
|
||||||
|
input { width: 100%; margin: 0.2rem 0 0.9rem; }
|
||||||
|
button { background: #174ea6; color: #fff; border: none; cursor: pointer; padding: 0.5rem 1rem; border-radius: 6px; }
|
||||||
|
button:hover { background: #0f3d91; }
|
||||||
|
#out { display: none; background: #101418; color: #d6f5d6; padding: 0.85rem 1rem;
|
||||||
|
border-radius: 8px; white-space: pre-wrap; word-break: break-all; margin-top: 1rem; }
|
||||||
|
.muted { color: #666; font-size: 0.92rem; }
|
||||||
|
ol { padding-left: 1.3rem; }
|
||||||
|
a { color: #174ea6; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Request FRX membership</h1>
|
<h1>FRX</h1>
|
||||||
|
<p class="tag">Federated Retrieval Exchange — a membership federation for retrieval.</p>
|
||||||
|
<p>Members answer broadcast queries from content they already hold. The protocol is deliberately
|
||||||
|
small: signed messages, budgets, honest truncation, aggregate courtesy. No announce stream, no
|
||||||
|
scores on the wire, no in-protocol payment.</p>
|
||||||
|
<p>Everything else — matching, ranking, retention, trust — is local.</p>
|
||||||
|
|
||||||
|
<h2>Join the federation</h2>
|
||||||
|
<div class="card">
|
||||||
<form id="f">
|
<form id="f">
|
||||||
<label>Organization or handle <input name="label" required pattern="[A-Za-z0-9-]+"></label><br>
|
<label>Organization or handle<br>
|
||||||
<label>Signup code <input name="code" type="password"></label><br>
|
<input name="label" required pattern="[A-Za-z0-9 -]+" placeholder="acme-docs"></label><br>
|
||||||
|
<label>Signup code (issued by the membership authority)<br>
|
||||||
|
<input name="code" type="password" placeholder="signup code"></label><br>
|
||||||
<button type="submit">Request membership</button>
|
<button type="submit">Request membership</button>
|
||||||
</form>
|
</form>
|
||||||
<pre id="out" style="white-space:pre-wrap"></pre>
|
<pre id="out"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>After you get credentials</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Install the single static binary: <code>frxd</code>.</li>
|
||||||
|
<li>Run <code>frxd --onboarding</code> and paste the credential block it gives you.</li>
|
||||||
|
<li>The wizard binds your keys, verifies the signed registry, and wires your relays — no domains, DNS, or ports needed on your side.</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Who is in</h2>
|
||||||
|
<p class="muted" id="members">…</p>
|
||||||
|
<p class="muted"><small>The registry is a signed, versioned, public snapshot: <code>GET /registry.json</code>. Membership is governed by the MA — questions and codes come from there.</small></p>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const out = document.getElementById("out");
|
||||||
document.getElementById("f").onsubmit = async (e) => {
|
document.getElementById("f").onsubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const label = e.target.label.value, code = e.target.code.value;
|
const label = e.target.label.value, code = e.target.code.value;
|
||||||
const res = await fetch("/v1/signup", {method: "POST", headers: {"content-type": "application/json"}, body: JSON.stringify({label, code})});
|
const res = await fetch("/v1/signup", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"content-type": "application/json"},
|
||||||
|
body: JSON.stringify({label, code})
|
||||||
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
document.getElementById("out").textContent = res.ok
|
out.style.display = "block";
|
||||||
? "Membership approved. Run `frxd --onboarding` and paste:\n\n" + body.credentials + "\n"
|
out.textContent = res.ok
|
||||||
: "Failed: " + (body.error || res.status);
|
? "Membership approved.\n\nRun `frxd --onboarding` and paste this block:\n\n" + body.credentials + "\n"
|
||||||
|
: "Failed: " + (body.error || ("http " + res.status));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/registry.json");
|
||||||
|
const doc = await res.json();
|
||||||
|
const ids = doc.members.map((m) => m.id);
|
||||||
|
document.getElementById("members").textContent = doc.members.length === 0
|
||||||
|
? "The registry is empty — be the first member."
|
||||||
|
: doc.members.length + " member(s): " + ids.join(", ") + " · registry v" + doc.version;
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById("members").textContent = "registry unavailable";
|
||||||
|
}
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body></html>
|
</body>
|
||||||
"#;
|
</html>
|
||||||
|
"##;
|
||||||
|
|
||||||
pub async fn status(config_path: &Path) -> Result<()> {
|
pub async fn status(config_path: &Path) -> Result<()> {
|
||||||
let config = Config::load(config_path)?;
|
let config = Config::load(config_path)?;
|
||||||
|
|||||||
+3
-1
@@ -18,6 +18,8 @@ struct Cli {
|
|||||||
config: PathBuf,
|
config: PathBuf,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
onboarding: bool,
|
onboarding: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
listen: Option<String>,
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<Command>,
|
command: Option<Command>,
|
||||||
}
|
}
|
||||||
@@ -204,7 +206,7 @@ async fn main() -> Result<()> {
|
|||||||
if cli.onboarding {
|
if cli.onboarding {
|
||||||
let mut input = std::io::stdin().lock();
|
let mut input = std::io::stdin().lock();
|
||||||
let mut output = std::io::stdout().lock();
|
let mut output = std::io::stdout().lock();
|
||||||
onboard::run(&mut input, &mut output, &cli.config).await?;
|
onboard::run(&mut input, &mut output, &cli.config, cli.listen.as_deref()).await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let Some(command) = cli.command else {
|
let Some(command) = cli.command else {
|
||||||
|
|||||||
+14
-1
@@ -13,6 +13,7 @@ pub async fn run(
|
|||||||
reader: &mut impl BufRead,
|
reader: &mut impl BufRead,
|
||||||
writer: &mut impl Write,
|
writer: &mut impl Write,
|
||||||
config_path: &Path,
|
config_path: &Path,
|
||||||
|
listen_override: Option<&str>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
writeln!(writer, "FRX onboarding")?;
|
writeln!(writer, "FRX onboarding")?;
|
||||||
writeln!(
|
writeln!(
|
||||||
@@ -150,7 +151,10 @@ pub async fn run(
|
|||||||
}
|
}
|
||||||
let data_dir = prompt(writer, reader, "data directory", "./frx-data")?;
|
let data_dir = prompt(writer, reader, "data directory", "./frx-data")?;
|
||||||
|
|
||||||
let mut config = Config::new(&id, "127.0.0.1:7701", relays.clone(), &data_dir);
|
let listen = listen_override
|
||||||
|
.map(str::to_string)
|
||||||
|
.unwrap_or_else(|| pick_listen("127.0.0.1:7701"));
|
||||||
|
let mut config = Config::new(&id, &listen, relays.clone(), &data_dir);
|
||||||
config.node.id = Some(id.clone());
|
config.node.id = Some(id.clone());
|
||||||
config.node.registry = Some(registry.clone());
|
config.node.registry = Some(registry.clone());
|
||||||
config.node.ma_key = Some(ma_key.clone());
|
config.node.ma_key = Some(ma_key.clone());
|
||||||
@@ -180,6 +184,7 @@ pub async fn run(
|
|||||||
writeln!(writer, " config: {}", config_path.display())?;
|
writeln!(writer, " config: {}", config_path.display())?;
|
||||||
writeln!(writer, " registry: {registry}")?;
|
writeln!(writer, " registry: {registry}")?;
|
||||||
writeln!(writer, " relays: {}", relays.join(", "))?;
|
writeln!(writer, " relays: {}", relays.join(", "))?;
|
||||||
|
writeln!(writer, " listening: {listen}")?;
|
||||||
writeln!(
|
writeln!(
|
||||||
writer,
|
writer,
|
||||||
"Next: frxd serve, then frx query \"...\" to broadcast."
|
"Next: frxd serve, then frx query \"...\" to broadcast."
|
||||||
@@ -228,6 +233,14 @@ fn parse_block(block: &str) -> Result<Credentials> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pick_listen(preferred: &str) -> String {
|
||||||
|
if std::net::TcpListener::bind(preferred).is_ok() {
|
||||||
|
return preferred.to_string();
|
||||||
|
}
|
||||||
|
let fallback = std::net::TcpListener::bind("127.0.0.1:0").expect("bind ephemeral port");
|
||||||
|
fallback.local_addr().expect("local addr").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
fn registry_base(registry_url: &str) -> String {
|
fn registry_base(registry_url: &str) -> String {
|
||||||
match registry_url.rfind('/') {
|
match registry_url.rfind('/') {
|
||||||
Some(index) => registry_url[..index].to_string(),
|
Some(index) => registry_url[..index].to_string(),
|
||||||
|
|||||||
+1
-1
@@ -120,7 +120,7 @@ async fn wizard_enrolls_and_writes_config() {
|
|||||||
let input = format!("{}\n{credentials}\n\n\n\nn\n", config_path.display());
|
let input = format!("{}\n{credentials}\n\n\n\nn\n", config_path.display());
|
||||||
let mut reader = input.as_bytes();
|
let mut reader = input.as_bytes();
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
onboard::run(&mut reader, &mut output, &config_path)
|
onboard::run(&mut reader, &mut output, &config_path, None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user