mirror of
https://github.com/fiatjaf/nak.git
synced 2026-10-05 15:18:23 +00:00
Resolve relative petnames using the selected signer
This commit is contained in:
@@ -20,4 +20,4 @@ jobs:
|
|||||||
# Existing CLI tests share flag state and include public-relay requests.
|
# Existing CLI tests share flag state and include public-relay requests.
|
||||||
# Keep this regression suite independent of those tests and Go vet's
|
# Keep this regression suite independent of those tests and Go vet's
|
||||||
# pre-existing non-constant-format diagnostics.
|
# pre-existing non-constant-format diagnostics.
|
||||||
- run: go test -vet=off . -run 'TestResolvePetname|TestParsePubKey' -count=1
|
- run: go test -vet=off . -run 'TestResolvePetname|TestParsePubKey|TestPetnameCLI' -count=1
|
||||||
|
|||||||
+1
-2
@@ -9,7 +9,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
"fiatjaf.com/nostr/keyer"
|
"fiatjaf.com/nostr/keyer"
|
||||||
"fiatjaf.com/nostr/nipb7/blossom"
|
"fiatjaf.com/nostr/nipb7/blossom"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
@@ -39,7 +38,7 @@ var blossomCmd = &cli.Command{
|
|||||||
var client *blossom.Client
|
var client *blossom.Client
|
||||||
pubkey := c.Args().First()
|
pubkey := c.Args().First()
|
||||||
if pubkey != "" {
|
if pubkey != "" {
|
||||||
pk, err := parsePubKey(pubkey, nostr.ZeroPK)
|
pk, err := parsePubKeyForCommand(ctx, c, pubkey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid public key '%s': %w", pubkey, err)
|
return fmt.Errorf("invalid public key '%s': %w", pubkey, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ aside from those, there is also:
|
|||||||
var owner nostr.PubKey
|
var owner nostr.PubKey
|
||||||
var ownerStr string
|
var ownerStr string
|
||||||
if c.String("owner") != "" {
|
if c.String("owner") != "" {
|
||||||
owner, err = parsePubKey(c.String("owner"), nostr.ZeroPK)
|
owner, err = parsePubKeyForCommand(ctx, c, c.String("owner"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid owner pubkey: %w", err)
|
return fmt.Errorf("invalid owner pubkey: %w", err)
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,7 @@ aside from those, there is also:
|
|||||||
}, &ownerStr); err != nil {
|
}, &ownerStr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
owner, err = parsePubKey(ownerStr, nostr.ZeroPK)
|
owner, err = parsePubKeyForCommand(ctx, c, ownerStr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ownerStr = nip19.EncodeNpub(owner)
|
ownerStr = nip19.EncodeNpub(owner)
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c
|
|||||||
keyGroups := make([][]*btcec.PublicKey, 0, len(result.Keys))
|
keyGroups := make([][]*btcec.PublicKey, 0, len(result.Keys))
|
||||||
|
|
||||||
for i, keyhex := range result.Keys {
|
for i, keyhex := range result.Keys {
|
||||||
pk32, err := parsePubKey(keyhex, nostr.ZeroPK)
|
pk32, err := parsePubKeyForCommand(ctx, c, keyhex)
|
||||||
if err == nil { /* we'll try both the 02 and the 03 prefix versions */
|
if err == nil { /* we'll try both the 02 and the 03 prefix versions */
|
||||||
result.Keys[i] = pk32.Hex()
|
result.Keys[i] = pk32.Hex()
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ var app = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
deferPetnameFlags(app)
|
||||||
|
|
||||||
cli.VersionFlag = &cli.BoolFlag{
|
cli.VersionFlag = &cli.BoolFlag{
|
||||||
Name: "version",
|
Name: "version",
|
||||||
Usage: "prints the version",
|
Usage: "prints the version",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ var outboxCmd = &cli.Command{
|
|||||||
return fmt.Errorf("expected exactly one argument (pubkey)")
|
return fmt.Errorf("expected exactly one argument (pubkey)")
|
||||||
}
|
}
|
||||||
|
|
||||||
pk, err := parsePubKey(c.Args().First(), nostr.ZeroPK)
|
pk, err := parsePubKeyForCommand(ctx, c, c.Args().First())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid public key '%s': %w", c.Args().First(), err)
|
return fmt.Errorf("invalid public key '%s': %w", c.Args().First(), err)
|
||||||
}
|
}
|
||||||
|
|||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip02"
|
||||||
|
"fiatjaf.com/nostr/nip05"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parsePubKeyForCommand(ctx context.Context, c *cli.Command, value string) (nostr.PubKey, error) {
|
||||||
|
value = strings.TrimPrefix(value, "nostr:")
|
||||||
|
from := nostr.ZeroPK
|
||||||
|
if strings.HasPrefix(value, "~") {
|
||||||
|
parts, err := nip02.ParsePetnamePath(value)
|
||||||
|
if err != nil {
|
||||||
|
return nostr.ZeroPK, err
|
||||||
|
}
|
||||||
|
// Absolute roots must also work without access to a signing key.
|
||||||
|
_, hexErr := nostr.PubKeyFromHex(parts[0])
|
||||||
|
prefix, _, nip19Err := nip19.Decode(parts[0])
|
||||||
|
absolute := hexErr == nil || nip05.IsValidIdentifier(parts[0]) ||
|
||||||
|
(nip19Err == nil && (prefix == "npub" || prefix == "nprofile"))
|
||||||
|
if !absolute {
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return nostr.ZeroPK, fmt.Errorf("failed to get petname identity: %w", err)
|
||||||
|
}
|
||||||
|
from, err = kr.GetPublicKey(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nostr.ZeroPK, fmt.Errorf("failed to get petname identity: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsePubKey(value, from)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public-key flags need the fully parsed signer options, including --sec after
|
||||||
|
// the petname flag. Keep their input until flag actions run, then let the
|
||||||
|
// original value parser handle the resolved keys (and any address inputs).
|
||||||
|
type deferredPubkeyFlag[T any, C any, V cli.ValueCreator[T, C]] struct {
|
||||||
|
*cli.FlagBase[T, C, V]
|
||||||
|
pending []string
|
||||||
|
value cli.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *deferredPubkeyFlag[T, C, V]) Apply(set *flag.FlagSet) error {
|
||||||
|
if err := f.FlagBase.Apply(set); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.pending = nil
|
||||||
|
f.value = set.Lookup(f.Name).Value.(cli.Value)
|
||||||
|
for _, name := range f.Names() {
|
||||||
|
set.Lookup(name).Value = &deferredPubkeyValue{Value: f.value, pending: &f.pending}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *deferredPubkeyFlag[T, C, V]) IsSet() bool {
|
||||||
|
return len(f.pending) > 0 || f.FlagBase.IsSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *deferredPubkeyFlag[T, C, V]) RunAction(ctx context.Context, c *cli.Command) error {
|
||||||
|
for _, input := range f.pending {
|
||||||
|
value := strings.TrimSpace(input)
|
||||||
|
if strings.HasPrefix(strings.TrimPrefix(value, "nostr:"), "~") {
|
||||||
|
pk, err := parsePubKeyForCommand(ctx, c, value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid value %q for --%s: %w", input, f.Name, err)
|
||||||
|
}
|
||||||
|
input = pk.Hex()
|
||||||
|
}
|
||||||
|
if err := f.value.Set(input); err != nil {
|
||||||
|
return fmt.Errorf("invalid value %q for --%s: %w", input, f.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.pending = nil
|
||||||
|
return f.FlagBase.RunAction(ctx, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
type deferredPubkeyValue struct {
|
||||||
|
cli.Value
|
||||||
|
pending *[]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *deferredPubkeyValue) Set(input string) error {
|
||||||
|
*v.pending = append(*v.pending, input)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deferPetnameFlag[T any, C any, V cli.ValueCreator[T, C]](f *cli.FlagBase[T, C, V]) cli.Flag {
|
||||||
|
return &deferredPubkeyFlag[T, C, V]{FlagBase: f}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deferPetnameFlags(c *cli.Command) {
|
||||||
|
for i, flag := range c.Flags {
|
||||||
|
switch flag := flag.(type) {
|
||||||
|
case *PubKeyFlag:
|
||||||
|
c.Flags[i] = deferPetnameFlag(flag)
|
||||||
|
case *PubKeySliceFlag:
|
||||||
|
c.Flags[i] = deferPetnameFlag(flag)
|
||||||
|
case *PubKeyOrAddressFlag:
|
||||||
|
c.Flags[i] = deferPetnameFlag(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, sub := range c.Commands {
|
||||||
|
deferPetnameFlags(sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/sdk"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPetnameCLI(t *testing.T) {
|
||||||
|
me := keyFromSeed(t, 1)
|
||||||
|
erin := keyFromSeed(t, 2)
|
||||||
|
david := keyFromSeed(t, 3)
|
||||||
|
abs := "~" + nip19.EncodeNpub(me.Public()) + "/erin"
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
}{
|
||||||
|
{"secret before flag", []string{"--sec", me.Hex(), "check", "--pubkey", "~/erin"}},
|
||||||
|
{"secret after flag", []string{"check", "--pubkey", "~/erin", "--sec", me.Hex()}},
|
||||||
|
{"relative chain", []string{"check", "--pubkey", "~erin/david", "--sec", me.Hex()}},
|
||||||
|
{"nostr prefix", []string{"check", "--pubkey", "nostr:~/erin", "--sec", me.Hex()}},
|
||||||
|
{"environment key", []string{"check", "--pubkey", "~/erin"}},
|
||||||
|
{"absolute without signer", []string{"check", "--pubkey", abs, "--sec", "invalid"}},
|
||||||
|
{"npub without signer", []string{"check", "--pubkey", nip19.EncodeNpub(erin.Public()), "--sec", "invalid"}},
|
||||||
|
{"positional", []string{"check", "~/erin", "--sec", me.Hex()}},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Setenv("NOSTR_SECRET_KEY", me.Hex())
|
||||||
|
setupPetnameSystem(t, map[nostr.SecretKey][]sdk.ProfileRef{
|
||||||
|
me: {{Pubkey: erin.Public(), Petname: "erin"}},
|
||||||
|
erin: {{Pubkey: david.Public(), Petname: "david"}},
|
||||||
|
})
|
||||||
|
called := false
|
||||||
|
cmd := &cli.Command{
|
||||||
|
Name: "nak", Writer: io.Discard, ErrWriter: io.Discard,
|
||||||
|
Flags: []cli.Flag{&cli.StringFlag{Name: "sec", Sources: cli.EnvVars("NOSTR_SECRET_KEY")}},
|
||||||
|
Commands: []*cli.Command{{
|
||||||
|
Name: "check",
|
||||||
|
Flags: []cli.Flag{&PubKeyFlag{Name: "pubkey"}},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
called = true
|
||||||
|
var got nostr.PubKey
|
||||||
|
if c.Args().Present() {
|
||||||
|
var err error
|
||||||
|
got, err = parsePubKeyForCommand(ctx, c, c.Args().First())
|
||||||
|
require.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
got = getPubKey(c, "pubkey")
|
||||||
|
}
|
||||||
|
want := erin.Public()
|
||||||
|
if tc.name == "relative chain" {
|
||||||
|
want = david.Public()
|
||||||
|
}
|
||||||
|
require.Equal(t, want, got)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
deferPetnameFlags(cmd)
|
||||||
|
require.NoError(t, cmd.Run(t.Context(), append([]string{"nak"}, tc.args...)))
|
||||||
|
require.True(t, called)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPetnameCLIRepeatedFlags(t *testing.T) {
|
||||||
|
me := keyFromSeed(t, 1)
|
||||||
|
erin := keyFromSeed(t, 2)
|
||||||
|
david := keyFromSeed(t, 3)
|
||||||
|
setupPetnameSystem(t, map[nostr.SecretKey][]sdk.ProfileRef{
|
||||||
|
me: {{Pubkey: erin.Public(), Petname: "erin"}},
|
||||||
|
})
|
||||||
|
addr := "30023:" + david.Public().Hex() + ":post"
|
||||||
|
cmd := &cli.Command{
|
||||||
|
Name: "nak", Writer: io.Discard, ErrWriter: io.Discard,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{Name: "sec"},
|
||||||
|
&PubKeySliceFlag{Name: "keys"},
|
||||||
|
&PubKeyOrAddressFlag{Name: "author", Aliases: []string{"a"}},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
require.Equal(t, []nostr.PubKey{erin.Public(), david.Public(), erin.Public()}, getPubKeySlice(c, "keys"))
|
||||||
|
authors := getPubKeyOrAddressSlice(c, "author")
|
||||||
|
require.Len(t, authors, 3)
|
||||||
|
require.Equal(t, erin.Public(), authors[0].PubKey)
|
||||||
|
require.Equal(t, david.Public(), authors[1].PubKey)
|
||||||
|
require.NotNil(t, authors[2].Addr)
|
||||||
|
require.Equal(t, addr, authors[2].Addr.AsTagReference())
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
deferPetnameFlags(cmd)
|
||||||
|
require.NoError(t, cmd.Run(t.Context(), []string{"nak",
|
||||||
|
"--keys", "~/erin", "--keys", david.Public().Hex(), "--keys", "~/erin",
|
||||||
|
"-a", "~/erin", "--author", david.Public().Hex(), "-a", addr, "--sec", me.Hex(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPetnameCLIInvalidIdentity(t *testing.T) {
|
||||||
|
setupPetnameSystem(t, nil)
|
||||||
|
cmd := &cli.Command{
|
||||||
|
Name: "nak", Writer: io.Discard, ErrWriter: io.Discard,
|
||||||
|
Flags: []cli.Flag{&cli.StringFlag{Name: "sec"}, &PubKeyFlag{Name: "pubkey"}},
|
||||||
|
Action: func(context.Context, *cli.Command) error {
|
||||||
|
t.Fatal("action must not run after a failed petname lookup")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
deferPetnameFlags(cmd)
|
||||||
|
err := cmd.Run(t.Context(), []string{"nak", "--pubkey", "~/erin", "--sec", "invalid"})
|
||||||
|
require.ErrorContains(t, err, "failed to get petname identity")
|
||||||
|
}
|
||||||
+1
-2
@@ -5,7 +5,6 @@ import (
|
|||||||
|
|
||||||
stdjson "encoding/json"
|
stdjson "encoding/json"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
"fiatjaf.com/nostr/nip19"
|
"fiatjaf.com/nostr/nip19"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
@@ -40,7 +39,7 @@ example usage:
|
|||||||
ArgsUsage: "[pubkey]",
|
ArgsUsage: "[pubkey]",
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for pubkeyInput := range getStdinLinesOrArguments(c.Args()) {
|
for pubkeyInput := range getStdinLinesOrArguments(c.Args()) {
|
||||||
pk, err := parsePubKey(pubkeyInput, nostr.ZeroPK)
|
pk, err := parsePubKeyForCommand(ctx, c, pubkeyInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx = lineProcessingError(ctx, "invalid pubkey '%s': %s", pubkeyInput, err)
|
ctx = lineProcessingError(ctx, "invalid pubkey '%s': %s", pubkeyInput, err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ func buildSpellReq(ctx context.Context, c *cli.Command, tags nostr.Tags) (nostr.
|
|||||||
|
|
||||||
getMe := func() (nostr.PubKey, error) {
|
getMe := func() (nostr.PubKey, error) {
|
||||||
if !c.IsSet("sec") && !c.IsSet("prompt-sec") && c.IsSet("pub") {
|
if !c.IsSet("sec") && !c.IsSet("prompt-sec") && c.IsSet("pub") {
|
||||||
return parsePubKey(c.String("pub"), nostr.ZeroPK)
|
return parsePubKeyForCommand(ctx, c, c.String("pub"))
|
||||||
}
|
}
|
||||||
|
|
||||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
|||||||
Reference in New Issue
Block a user