refactor(cli): remove the graperank sync alias; reject non-64-hex user ids

`graperank sync` is gone — `crawl` is the only spelling. Because the dispatch
treats any non-verb first token as an OBSERVER, this exposed a latent leniency
in the shared resolver: decodePublicKeyAsHexOrNull's fallback runs Hex.decode
without a length check, so a short bech32/hex-ish word like `sync` decodes to a
bogus few-byte "pubkey" instead of failing — turning `graperank sync` into a
silent network crawl over garbage. Context.requireUserHex now requires the
resolved value to be exactly 64 hex chars (a pubkey is always 32 bytes), so a
mistyped OBSERVER/USER on any amy command errors cleanly (exit 2) instead of
silently scoring/fetching against a garbage key. `graperank sync` now returns
`bad_args` rather than crawling.

Also drops the never-built `graperank compare` idea from the roadmap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WSzVX9RoUxyV3nT3dfc56
This commit is contained in:
Claude
2026-07-14 21:33:49 +00:00
parent 883365d6a5
commit 65eac0f359
2 changed files with 15 additions and 16 deletions
@@ -470,11 +470,22 @@ class Context(
* accept the exact same identifier formats. Throws on unrecognised input —
* command handlers catch [IllegalArgumentException] at the top level and
* translate to `{"error": "bad_args"}`.
*
* A pubkey is always 32 bytes, so we require exactly 64 hex chars: the shared
* resolver's fallback runs a lenient `Hex.decode` that turns a short
* bech32/hex-ish word (e.g. a mistyped verb like `sync`) into a bogus few-byte
* "pubkey" instead of failing — this rejects that so a bad OBSERVER/USER errors
* cleanly rather than silently scoring/fetching garbage.
*/
suspend fun requireUserHex(input: String): com.vitorpamplona.quartz.nip01Core.core.HexKey =
com.vitorpamplona.quartz.nip05DnsIdentifiers
.resolveUserHexOrNull(input, nip05Client)
?: throw IllegalArgumentException("Could not resolve user: '$input' (accepts npub, nprofile, 64-hex, or name@domain.tld)")
suspend fun requireUserHex(input: String): com.vitorpamplona.quartz.nip01Core.core.HexKey {
val notResolved = "Could not resolve user: '$input' (accepts npub, nprofile, 64-hex, or name@domain.tld)"
val hex =
com.vitorpamplona.quartz.nip05DnsIdentifiers
.resolveUserHexOrNull(input, nip05Client)
?: throw IllegalArgumentException(notResolved)
require(hex.length == 64) { notResolved }
return hex
}
/**
* Outbox / NIP-65 write relays for this account. Read from the
@@ -85,7 +85,6 @@ import kotlin.math.roundToInt
* - `amy graperank crawl [OBSERVER]` — network only: crawl the reachable graph's
* kind 3/10000/1984/10002 into the local store. Idempotent and cumulative, so
* run it a few times to make sure everything is loaded. Scores nothing.
* (`sync` is a deprecated alias — it warns and will be removed.)
* - `amy graperank status` — read-only inventory of all of the above: WoT record
* counts, reachability-cache freshness, operator state, persisted card sets.
* Answers "do I need to crawl again?" with no network and no signing.
@@ -210,17 +209,6 @@ object GrapeRankCommand {
"providers" -> providers(dataDir, tail.drop(1).toTypedArray())
"operator" -> operator(dataDir, tail.drop(1).toTypedArray())
"crawl" -> crawl(dataDir, tail.drop(1).toTypedArray())
// Deprecated pre-rename alias for `crawl`. Actively misleading now that
// the negentropy record refresh is `graperank refresh` (and `amy sync`
// is the generic negentropy verb), so it warns before it runs — next
// step is removal.
"sync" -> {
System.err.println(
"[graperank] `graperank sync` is deprecated and will be removed — use `graperank crawl` " +
"(or `graperank refresh` to re-sync known authors' records).",
)
crawl(dataDir, tail.drop(1).toTypedArray())
}
"status" -> status(dataDir)
// The relay census outgrew graperank (it feeds the shared NIP-66
// reachability cache every command reads) and moved to `amy relay