diff --git a/README.md b/README.md index 3807845..34451f2 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ would only work in one of those places. ## CLI ```sh -moshpit-resolve [--moshpit] [--clearnet-resolves] [--registry URL] [--console URL] [--parking URL] [--timeout MS] [--json] +moshpit-resolve [--moshpit] [--clearnet-resolves] [--registry URL] [--console URL] [--parking URL] [--timeout MS] [--strict] [--json] ``` ``` @@ -84,6 +84,11 @@ every navigation, and the thing you need when a name goes somewhere unexpected. Use `--json` when another tool needs the registry answer, decision, reason, and destination without parsing the human-readable summary. +By default, an unavailable registry still exits successfully after reporting +the clearnet fallback. Scripts can add `--strict` to exit non-zero when the +registry lookup is inconclusive. Resolved, parked, registry-backed clearnet, +and reserved console decisions still exit successfully. + Registry lookups use an eight-second deadline by default. Scripts and self-hosted deployments can lower it without changing the resolution policy: diff --git a/bin/moshpit-resolve.mjs b/bin/moshpit-resolve.mjs index d576abd..46758f4 100755 --- a/bin/moshpit-resolve.mjs +++ b/bin/moshpit-resolve.mjs @@ -10,7 +10,7 @@ import { const USAGE = `moshpit-resolve — where a Moshpit name would send you - moshpit-resolve [--moshpit] [--clearnet-resolves] [--registry URL] [--console URL] [--parking URL] [--timeout MS] + moshpit-resolve [--moshpit] [--clearnet-resolves] [--registry URL] [--console URL] [--parking URL] [--timeout MS] [--strict] --moshpit let a registered name beat a clearnet answer --clearnet-resolves pretend the real internet has an answer for this name @@ -18,6 +18,7 @@ const USAGE = `moshpit-resolve — where a Moshpit name would send you --console URL a custom namespace management console --parking URL a custom base for unpointed names --timeout MS registry request deadline (default: ${DEFAULT_LOOKUP_TIMEOUT_MS}) + --strict fail when the registry lookup is inconclusive --json print a machine-readable resolution decision Prints the destination and the reason for it. No browser, no navigation.`; @@ -83,3 +84,7 @@ if (raw) { console.log(` reason ${decision.reason}`); console.log(` goes to ${url ?? "(nowhere — the browser keeps its own answer)"}`); } + +if (flag("strict") && decision.reason === "Moshpit registry not consulted or unreachable") { + process.exitCode = 1; +} diff --git a/test/cli.test.mjs b/test/cli.test.mjs index 9aa3103..e943457 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -216,3 +216,109 @@ test("--timeout aborts a slow registry lookup", async (t) => { assert.equal(requests, 1); assert.ok(elapsed < 1500, `configured timeout took ${elapsed}ms`); }); + +test("--strict reports an unavailable registry through the exit status", async (t) => { + let requests = 0; + const server = createServer((_request, response) => { + requests++; + response.writeHead(503); + response.end(); + }); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + t.after(() => new Promise((resolve) => server.close(resolve))); + + const args = [ + "blue.eggs", + "--registry", `http://127.0.0.1:${server.address().port}`, + "--json", + ]; + const normal = await run(args); + const strict = await run([...args, "--strict"]); + const strictOverride = await run([ + ...args, + "--moshpit", + "--clearnet-resolves", + "--strict", + ]); + + assert.equal(normal.status, 0, normal.stderr || normal.stdout); + assert.equal(strict.status, 1); + assert.equal(strictOverride.status, 1); + assert.equal(strict.stderr, ""); + assert.deepEqual(JSON.parse(strict.stdout), JSON.parse(normal.stdout)); + assert.deepEqual(JSON.parse(strict.stdout), { + name: "blue.eggs", + registry: null, + decision: { + use: "clearnet", + reason: "Moshpit registry not consulted or unreachable", + }, + destination: null, + }); + assert.equal(JSON.parse(strictOverride.stdout).decision.reason, + "Moshpit registry not consulted or unreachable"); + assert.equal(requests, 3); +}); + +test("--strict keeps conclusive resolution paths successful", async (t) => { + let requests = 0; + const server = createServer((request, response) => { + requests++; + const name = new URL(request.url, "http://127.0.0.1").searchParams.get("name"); + const parked = name === "parked.eggs"; + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify({ + name_registered: !parked, + resolved: name, + target: parked ? null : "203.0.113.9", + })); + }); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + t.after(() => new Promise((resolve) => server.close(resolve))); + + const registry = `http://127.0.0.1:${server.address().port}`; + const cases = [ + ["mosh.eggs", "--console", "https://console.example", "--strict", "--json"], + ["mosh.eggs", "--clearnet-resolves", "--strict", "--json"], + ["parked.eggs", "--registry", registry, "--strict", "--json"], + ["live.eggs", "--registry", registry, "--moshpit", "--strict", "--json"], + ["live.eggs", "--registry", registry, "--clearnet-resolves", "--strict", "--json"], + ]; + + const results = await Promise.all(cases.map(run)); + for (const result of results) { + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.equal(result.stderr, ""); + } + + const registryBackedClearnet = JSON.parse(results.at(-1).stdout); + assert.equal(registryBackedClearnet.decision.use, "clearnet"); + assert.ok(registryBackedClearnet.registry); + assert.equal(registryBackedClearnet.destination, null); + assert.equal(requests, 3); +}); + +test("--strict preserves human-readable output before failing", async (t) => { + const server = createServer((_request, response) => { + response.writeHead(503); + response.end(); + }); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + t.after(() => new Promise((resolve) => server.close(resolve))); + + const result = await run([ + "blue.eggs", + "--registry", `http://127.0.0.1:${server.address().port}`, + "--strict", + ]); + + assert.equal(result.status, 1); + assert.equal(result.stderr, ""); + assert.match(result.stdout, /^blue\.eggs\n/); + assert.match(result.stdout, /registry\s+unreachable/); + assert.match(result.stdout, /decision\s+clearnet/); + assert.match(result.stdout, /goes to\s+\(nowhere/); +});