mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
feat(cli): add nak-style stateless primitives (decode, encode, verify, key)
Add the first batch of nak parity commands to amy — the army-knife primitives that operate purely on their arguments, with no account or network. They dispatch before account resolution (like `use`), so they run with zero `~/.amy/` state: - `amy decode ENTITY` NIP-19/21 entity -> JSON - `amy encode <type> …` raw parts -> NIP-19 entity - `amy verify [JSON]` id-hash + signature check (reads stdin) - `amy key generate|public` mint a keypair / derive a pubkey All four are thin wrappers over quartz (Nip19Parser, the NIP-19 entities, Event.verifyId/verifySignature, KeyPair) per the cli thin-assembly rule. README + ROADMAP updated with a nak-parity matrix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
This commit is contained in:
@@ -197,6 +197,23 @@ $ amy relay publish-lists # broadcast updated kind:10002/10050/10051
|
||||
|
||||
## Commands
|
||||
|
||||
### Primitives (stateless — no account or network)
|
||||
|
||||
Army-knife verbs that operate purely on their arguments. They never touch
|
||||
`~/.amy/`, so they run with zero state — handy for scripting and piping
|
||||
(`amy decode … | jq`, `… | amy verify`).
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `amy decode ENTITY` | Decode a NIP-19/21 entity (`npub`/`nsec`/`note`/`nevent`/`nprofile`/`naddr`/`nrelay`/`nembed`) to JSON. Accepts an optional `nostr:` prefix. |
|
||||
| `amy encode npub HEX` / `nsec HEX` / `note ID` | Encode a single 32-byte hex value into the matching NIP-19 entity. |
|
||||
| `amy encode nevent ID [--author HEX] [--kind N] [--relay URL[,URL…]]` | Encode an event pointer with optional author/kind/relay hints. |
|
||||
| `amy encode nprofile HEX [--relay URL[,URL…]]` | Encode a profile pointer with optional relay hints. |
|
||||
| `amy encode naddr --kind N --pubkey HEX --identifier D [--relay URL[,URL…]]` | Encode an addressable-event (`a` tag) pointer. |
|
||||
| `amy verify [EVENT-JSON]` | Check an event's id hash and signature. Reads stdin when the argument is omitted or `-`. Reports `id_ok` + `signature_ok` separately. |
|
||||
| `amy key generate` | Mint a fresh keypair (`nsec` + `npub` + hex). Does not persist — use `init`/`login` for that. |
|
||||
| `amy key public NSEC\|HEX` | Derive the public key from a secret key. |
|
||||
|
||||
### Identity
|
||||
|
||||
| Command | What it does |
|
||||
|
||||
@@ -70,6 +70,32 @@ Status legend: ✅ shipped · 📦 logic lives in `commons/`, needs a command ·
|
||||
| Notifications feed | 🆕 | |
|
||||
| Search (NIP-50) | 🆕 | |
|
||||
|
||||
### `nak` parity — army-knife primitives
|
||||
|
||||
Tracking [`fiatjaf/nak`](https://github.com/fiatjaf/nak)'s command surface. amy
|
||||
adapts the verbiage where its own conventions differ (`req` → one-shot `fetch`
|
||||
vs streaming `subscribe`). Stateless verbs run with no account or network.
|
||||
|
||||
| nak command | amy verb | Status | Notes |
|
||||
|---|---|---|---|
|
||||
| `decode` | `amy decode` | ✅ | NIP-19/21 → JSON. Quartz `Nip19Parser`. |
|
||||
| `encode` | `amy encode` | ✅ | npub/nsec/note/nevent/nprofile/naddr. |
|
||||
| `verify` / `validate` | `amy verify` | ✅ | id-hash + signature, reported separately. |
|
||||
| `key` | `amy key generate\|public` | ✅ in part · 🆕 | generate + derive done; NIP-49 encrypt/decrypt pending. |
|
||||
| `event` | `amy event` | 🆕 | build/sign an arbitrary event, optional `--publish`. |
|
||||
| `publish` | `amy publish` | 🆕 | broadcast a pre-made event JSON. |
|
||||
| `req` (one-shot) | `amy fetch` | 🆕 | filter → collect-until-EOSE. |
|
||||
| `req` (stream) | `amy subscribe` | 🆕 | filter → live stream to stdout. |
|
||||
| `count` | `amy count` | 🆕 | NIP-45. |
|
||||
| `encrypt` / `decrypt` | `amy encrypt\|decrypt` | 🆕 | raw NIP-44 / NIP-04. |
|
||||
| `gift` | `amy gift wrap\|unwrap` | 🆕 | NIP-59 primitive (gift-wrap path already used by `dm`). |
|
||||
| `relay` (NIP-11) | `amy relay info` | 🆕 | amy's `relay` is config today; add an `info` verb. |
|
||||
| `outbox` | `amy outbox` | 🆕 | NIP-65 relay discovery for a user. |
|
||||
| `blossom` | `amy blossom` | 🆕 | upload/download/list/delete (client already used by `dm`/`nsite`). |
|
||||
| `kind` / `nip` | `amy kind` / `amy nip` | 🆕 | reference lookups. |
|
||||
| `sync` | `amy relay sync` | 🆕 | NIP-77 Negentropy. |
|
||||
| `bunker` / `serve` / `admin` / `wallet` / `git` / `podcast` / `mcp` / `fs` / `spell` | — | 🆕 (tier 2/3) | larger/niche; some pull new deps. |
|
||||
|
||||
---
|
||||
|
||||
## Order of operations
|
||||
|
||||
@@ -122,6 +122,24 @@ private suspend fun dispatch(argv: Array<String>): Int {
|
||||
.run(tail)
|
||||
}
|
||||
|
||||
// Stateless local primitives (nak-style army-knife verbs). They operate
|
||||
// purely on their arguments — no identity, no relays, no `~/.amy/` — so
|
||||
// they dispatch before account resolution and work with zero state.
|
||||
when (head) {
|
||||
"decode" ->
|
||||
return com.vitorpamplona.amethyst.cli.commands.DecodeCommand
|
||||
.run(tail)
|
||||
"encode" ->
|
||||
return com.vitorpamplona.amethyst.cli.commands.EncodeCommand
|
||||
.run(tail)
|
||||
"verify" ->
|
||||
return com.vitorpamplona.amethyst.cli.commands.VerifyCommand
|
||||
.run(tail)
|
||||
"key" ->
|
||||
return com.vitorpamplona.amethyst.cli.commands.KeyCommands
|
||||
.dispatch(tail)
|
||||
}
|
||||
|
||||
val secrets = SecretStore.from(backendFlag = secretBackendFlag, passphraseFile = passphraseFileFlag)
|
||||
val dataDir = DataDir.resolve(accountFlag = accountFlag, secrets = secrets)
|
||||
|
||||
@@ -333,6 +351,20 @@ private fun printUsage() {
|
||||
| then ${'$'}AMY_PASSPHRASE, then a TTY prompt. `plaintext` writes the
|
||||
| private key directly into identity.json (still 0600) — dev only.
|
||||
|
|
||||
|Primitives (stateless — no account or network needed):
|
||||
| decode ENTITY decode a NIP-19/21 entity (npub|nsec|note|nevent|
|
||||
| nprofile|naddr|nrelay|nembed) to JSON
|
||||
| encode npub HEX encode raw parts into a NIP-19 entity:
|
||||
| encode nsec HEX nevent/nprofile/naddr accept --relay URL[,URL…];
|
||||
| encode note ID nevent accepts --author HEX --kind N;
|
||||
| encode nevent ID [...] naddr needs --kind N --pubkey HEX --identifier D
|
||||
| encode nprofile HEX [...]
|
||||
| encode naddr --kind N --pubkey HEX --identifier D [--relay URL[,URL…]]
|
||||
| verify [EVENT-JSON] check an event's id hash + signature
|
||||
| (reads stdin when the arg is omitted or `-`)
|
||||
| key generate mint a fresh keypair (nsec + npub + hex)
|
||||
| key public NSEC|HEX derive the public key from a secret key
|
||||
|
|
||||
|Identity:
|
||||
| init [--nsec NSEC] create or import a bare identity (no defaults published)
|
||||
| create [--name NAME] provision a full Amethyst-style account + publish bootstrap events
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.cli.commands
|
||||
|
||||
import com.vitorpamplona.amethyst.cli.Args
|
||||
import com.vitorpamplona.amethyst.cli.Output
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
|
||||
/**
|
||||
* `amy decode <bech32>` — turn a NIP-19 / NIP-21 entity into its structured
|
||||
* parts (nak's `decode`). Local, no network, no account. Accepts an optional
|
||||
* `nostr:` prefix.
|
||||
*
|
||||
* Thin assembly only: all parsing lives in quartz's [Nip19Parser]; this file
|
||||
* just maps the parsed entity onto amy's result-map output contract.
|
||||
*/
|
||||
object DecodeCommand {
|
||||
fun run(rest: Array<String>): Int {
|
||||
val args = Args(rest)
|
||||
val input = args.positional(0, "entity").trim()
|
||||
|
||||
val entity =
|
||||
Nip19Parser.uriToRoute(input)?.entity
|
||||
?: return Output.error("bad_args", "not a recognized NIP-19 entity (npub, nsec, note, nevent, nprofile, naddr, nrelay, nembed)")
|
||||
|
||||
val result: Map<String, Any?> =
|
||||
when (entity) {
|
||||
is NPub -> mapOf("type" to "npub", "pubkey" to entity.hex)
|
||||
is NSec -> mapOf("type" to "nsec", "private_key" to entity.hex, "pubkey" to entity.toPubKeyHex())
|
||||
is NNote -> mapOf("type" to "note", "id" to entity.hex)
|
||||
is NProfile ->
|
||||
mapOf(
|
||||
"type" to "nprofile",
|
||||
"pubkey" to entity.hex,
|
||||
"relays" to entity.relay.map { it.url },
|
||||
)
|
||||
is NEvent ->
|
||||
mapOf(
|
||||
"type" to "nevent",
|
||||
"id" to entity.hex,
|
||||
"author" to entity.author,
|
||||
"kind" to entity.kind,
|
||||
"relays" to entity.relay.map { it.url },
|
||||
)
|
||||
is NAddress ->
|
||||
mapOf(
|
||||
"type" to "naddr",
|
||||
"kind" to entity.kind,
|
||||
"pubkey" to entity.author,
|
||||
"identifier" to entity.dTag,
|
||||
"relays" to entity.relay.map { it.url },
|
||||
)
|
||||
is NRelay -> mapOf("type" to "nrelay", "relays" to entity.relay)
|
||||
is NEmbed -> mapOf("type" to "nembed", "event" to Output.mapper.readTree(entity.event.toJson()))
|
||||
else -> return Output.error("bad_args", "unsupported entity type")
|
||||
}
|
||||
|
||||
Output.emit(result)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.cli.commands
|
||||
|
||||
import com.vitorpamplona.amethyst.cli.Args
|
||||
import com.vitorpamplona.amethyst.cli.Output
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNsec
|
||||
|
||||
/**
|
||||
* `amy encode <type> …` — build a NIP-19 entity from raw parts (nak's
|
||||
* `encode`). Local, no network, no account.
|
||||
*
|
||||
* encode npub <hex>
|
||||
* encode nsec <hex>
|
||||
* encode note <event-id-hex>
|
||||
* encode nevent <event-id-hex> [--author HEX] [--kind N] [--relay URL[,URL…]]
|
||||
* encode nprofile <pubkey-hex> [--relay URL[,URL…]]
|
||||
* encode naddr --kind N --pubkey HEX --identifier D [--relay URL[,URL…]]
|
||||
*
|
||||
* Thin assembly only: every encoder lives in quartz's NIP-19 entities; this
|
||||
* file parses flags and calls them.
|
||||
*/
|
||||
object EncodeCommand {
|
||||
fun run(rest: Array<String>): Int {
|
||||
if (rest.isEmpty()) return Output.error("bad_args", "encode <npub|nsec|note|nevent|nprofile|naddr> …")
|
||||
val type = rest[0]
|
||||
val args = Args(rest.drop(1).toTypedArray())
|
||||
|
||||
return when (type) {
|
||||
"npub" -> emit("npub", NPub.create(hex32(args.positional(0, "pubkey-hex"))))
|
||||
"nsec" -> emit("nsec", hex32(args.positional(0, "private-key-hex")).hexToByteArray().toNsec())
|
||||
"note" -> emit("note", NNote.create(hex32(args.positional(0, "event-id-hex"))))
|
||||
"nevent" ->
|
||||
emit(
|
||||
"nevent",
|
||||
NEvent.create(
|
||||
idHex = hex32(args.positional(0, "event-id-hex")),
|
||||
author = args.flag("author"),
|
||||
kind = args.flag("kind")?.toIntOrNull(),
|
||||
relays = relays(args),
|
||||
),
|
||||
)
|
||||
"nprofile" ->
|
||||
emit(
|
||||
"nprofile",
|
||||
NProfile.create(
|
||||
authorPubKeyHex = hex32(args.positional(0, "pubkey-hex")),
|
||||
relays = relays(args),
|
||||
),
|
||||
)
|
||||
"naddr" ->
|
||||
emit(
|
||||
"naddr",
|
||||
NAddress.create(
|
||||
kind = args.requireFlag("kind").toIntOrNull() ?: return Output.error("bad_args", "--kind must be an integer"),
|
||||
pubKeyHex = hex32(args.requireFlag("pubkey")),
|
||||
dTag = args.flag("identifier", "") ?: "",
|
||||
relays = relays(args),
|
||||
),
|
||||
)
|
||||
else -> Output.error("bad_args", "encode $type (expected npub|nsec|note|nevent|nprofile|naddr)")
|
||||
}
|
||||
}
|
||||
|
||||
/** Validate a 64-char hex key/id; bare-hex only — bech32 inputs go through `decode` first. */
|
||||
private fun hex32(value: String): String {
|
||||
val v = value.trim().lowercase()
|
||||
require(v.length == 64 && v.all { it in "0123456789abcdef" }) {
|
||||
"expected 64-char hex, got '$value'"
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
private fun relays(args: Args): List<NormalizedRelayUrl> =
|
||||
args
|
||||
.flag("relay")
|
||||
?.split(',')
|
||||
?.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it.trim()) }
|
||||
.orEmpty()
|
||||
|
||||
private fun emit(
|
||||
key: String,
|
||||
value: String,
|
||||
): Int {
|
||||
Output.emit(mapOf(key to value))
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.cli.commands
|
||||
|
||||
import com.vitorpamplona.amethyst.cli.Args
|
||||
import com.vitorpamplona.amethyst.cli.Identity
|
||||
import com.vitorpamplona.amethyst.cli.Output
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
|
||||
/**
|
||||
* `amy key …` — standalone key utilities (nak's `key`). Local, no network,
|
||||
* no account; these never touch `~/.amy/` — they operate purely on the keys
|
||||
* passed in. Use `amy init` / `amy login` to persist an identity.
|
||||
*
|
||||
* key generate mint a fresh keypair (prints nsec + npub + hex)
|
||||
* key public <nsec|hex-priv> derive the public key from a secret key
|
||||
*
|
||||
* Thin assembly only: key generation + bech32 derivation live in quartz
|
||||
* (reused here via [Identity], which wraps Quartz's `KeyPair`).
|
||||
*/
|
||||
object KeyCommands {
|
||||
fun dispatch(rest: Array<String>): Int {
|
||||
if (rest.isEmpty()) return Output.error("bad_args", "key <generate|public>")
|
||||
val tail = rest.drop(1).toTypedArray()
|
||||
return when (rest[0]) {
|
||||
"generate" -> generate()
|
||||
"public" -> public(tail)
|
||||
else -> Output.error("bad_args", "key ${rest[0]} (expected generate|public)")
|
||||
}
|
||||
}
|
||||
|
||||
private fun generate(): Int {
|
||||
val id = Identity.create()
|
||||
Output.emit(
|
||||
mapOf(
|
||||
"nsec" to id.nsec,
|
||||
"npub" to id.npub,
|
||||
"private_key" to id.privKeyHex,
|
||||
"pubkey" to id.pubKeyHex,
|
||||
),
|
||||
)
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun public(rest: Array<String>): Int {
|
||||
val args = Args(rest)
|
||||
val input = args.positional(0, "secret-key").trim()
|
||||
val id =
|
||||
try {
|
||||
when {
|
||||
input.startsWith("nsec") -> Identity.fromNsec(input)
|
||||
input.length == 64 && input.lowercase().all { it in "0123456789abcdef" } ->
|
||||
Identity.fromPrivateKey(input.hexToByteArray())
|
||||
else -> return Output.error("bad_args", "expected an nsec or a 64-char hex private key")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return Output.error("bad_args", "could not parse secret key: ${e.message}")
|
||||
}
|
||||
Output.emit(mapOf("npub" to id.npub, "pubkey" to id.pubKeyHex))
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.cli.commands
|
||||
|
||||
import com.vitorpamplona.amethyst.cli.Args
|
||||
import com.vitorpamplona.amethyst.cli.Output
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.verifyId
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.verifySignature
|
||||
|
||||
/**
|
||||
* `amy verify [EVENT-JSON]` — check a Nostr event's id hash and signature
|
||||
* (nak's `verify`). Local, no network, no account. The event JSON is read
|
||||
* from the positional argument, or from stdin when the argument is omitted
|
||||
* or `-`.
|
||||
*
|
||||
* Reports `id_ok` (computed id matches the `id` field) and `signature_ok`
|
||||
* separately so a caller can tell a tampered id apart from a bad sig.
|
||||
* `valid` is the conjunction. Exit code stays 0 — the result is data, not
|
||||
* a runtime failure (parse errors are runtime/bad_args).
|
||||
*/
|
||||
object VerifyCommand {
|
||||
fun run(rest: Array<String>): Int {
|
||||
val args = Args(rest)
|
||||
val arg = args.positionalOrNull(0)
|
||||
val json =
|
||||
if (arg == null || arg == "-") {
|
||||
System.`in`
|
||||
.readBytes()
|
||||
.decodeToString()
|
||||
.trim()
|
||||
} else {
|
||||
arg.trim()
|
||||
}
|
||||
if (json.isEmpty()) return Output.error("bad_args", "no event JSON on the argument or stdin")
|
||||
|
||||
val event =
|
||||
try {
|
||||
Event.fromJson(json)
|
||||
} catch (e: Exception) {
|
||||
return Output.error("bad_args", "could not parse event JSON: ${e.message}")
|
||||
}
|
||||
|
||||
val idOk = event.verifyId()
|
||||
val sigOk = event.verifySignature()
|
||||
|
||||
Output.emit(
|
||||
mapOf(
|
||||
"valid" to (idOk && sigOk),
|
||||
"id" to event.id,
|
||||
"id_ok" to idOk,
|
||||
"signature_ok" to sigOk,
|
||||
),
|
||||
)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user