mirror of
https://github.com/fiatjaf/nak.git
synced 2026-10-05 15:18:23 +00:00
petname support in parsePubKey().
This commit is contained in:
+2
-1
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"unsafe"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/keyer"
|
||||
"fiatjaf.com/nostr/nipb7/blossom"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -39,7 +40,7 @@ var blossomCmd = &cli.Command{
|
||||
var client *blossom.Client
|
||||
pubkey := c.Args().First()
|
||||
if pubkey != "" {
|
||||
pk, err := parsePubKey(pubkey)
|
||||
pk, err := parsePubKey(pubkey, nostr.ZeroPK)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid public key '%s': %w", pubkey, err)
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ func (t pubkeyValue) Create(val nostr.PubKey, p *nostr.PubKey, c struct{}) cli.V
|
||||
func (t pubkeyValue) ToString(b nostr.PubKey) string { return t.pubkey.String() }
|
||||
|
||||
func (t *pubkeyValue) Set(value string) error {
|
||||
pubkey, err := parsePubKey(value)
|
||||
pubkey, err := parsePubKey(value, nostr.ZeroPK)
|
||||
t.pubkey = pubkey
|
||||
t.hasBeenSet = true
|
||||
return err
|
||||
@@ -255,7 +255,7 @@ func (t pubKeyOrAddressValue) ToString(b PubKeyOrAddress) string {
|
||||
|
||||
func (t *pubKeyOrAddressValue) Set(value string) error {
|
||||
value = strings.TrimPrefix(value, "nostr:")
|
||||
pubkey, err1 := parsePubKey(value)
|
||||
pubkey, err1 := parsePubKey(value, nostr.ZeroPK)
|
||||
if err1 == nil {
|
||||
t.value = PubKeyOrAddress{PubKey: pubkey}
|
||||
t.hasBeenSet = true
|
||||
|
||||
@@ -156,7 +156,7 @@ aside from those, there is also:
|
||||
var owner nostr.PubKey
|
||||
var ownerStr string
|
||||
if c.String("owner") != "" {
|
||||
owner, err = parsePubKey(c.String("owner"))
|
||||
owner, err = parsePubKey(c.String("owner"), nostr.ZeroPK)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid owner pubkey: %w", err)
|
||||
}
|
||||
@@ -169,7 +169,7 @@ aside from those, there is also:
|
||||
}, &ownerStr); err != nil {
|
||||
return err
|
||||
}
|
||||
owner, err = parsePubKey(ownerStr)
|
||||
owner, err = parsePubKey(ownerStr, nostr.ZeroPK)
|
||||
if err == nil {
|
||||
ownerStr = nip19.EncodeNpub(owner)
|
||||
break
|
||||
@@ -2204,7 +2204,7 @@ please fix
|
||||
}
|
||||
|
||||
// parse owner
|
||||
owner, err := parsePubKey(localConfig.Owner)
|
||||
owner, err := parsePubKey(localConfig.Owner, nostr.ZeroPK)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid owner public key: %w", err)
|
||||
}
|
||||
@@ -3046,7 +3046,7 @@ func gitSync(ctx context.Context, signer nostr.Keyer, skipAnnouncement bool) (ni
|
||||
}
|
||||
|
||||
// parse owner
|
||||
owner, err := parsePubKey(localConfig.Owner)
|
||||
owner, err := parsePubKey(localConfig.Owner, nostr.ZeroPK)
|
||||
if err != nil {
|
||||
return nip34.Repository{}, nil, fmt.Errorf("invalid owner public key: %w", err)
|
||||
}
|
||||
@@ -3573,7 +3573,7 @@ func parseRepositoryAddress(
|
||||
parts := strings.Split(address, "/")
|
||||
if len(parts) == 5 {
|
||||
// nostr://<owner>/<relay>/<identifier>
|
||||
owner, err = parsePubKey(parts[2])
|
||||
owner, err = parsePubKey(parts[2], nostr.ZeroPK)
|
||||
if err != nil {
|
||||
return nostr.PubKey{}, "", nil, fmt.Errorf("invalid owner in URL: %w", err)
|
||||
}
|
||||
@@ -3596,7 +3596,7 @@ func parseRepositoryAddress(
|
||||
return owner, identifier, relayHints, nil
|
||||
} else if len(parts) == 4 {
|
||||
// nostr://<owner>/<identifier>
|
||||
owner, err = parsePubKey(parts[2])
|
||||
owner, err = parsePubKey(parts[2], nostr.ZeroPK)
|
||||
if err != nil {
|
||||
return nostr.PubKey{}, "", nil, fmt.Errorf("invalid owner in URL: %w", err)
|
||||
}
|
||||
@@ -3625,7 +3625,7 @@ func parseRepositoryAddress(
|
||||
identifier = parts[1]
|
||||
|
||||
// try to parse as pubkey (npub, nprofile, or hex)
|
||||
owner, err = parsePubKey(ownerPart)
|
||||
owner, err = parsePubKey(ownerPart, nostr.ZeroPK)
|
||||
if err != nil {
|
||||
return nostr.PubKey{}, "", nil, fmt.Errorf("invalid owner identifier '%s': %w", ownerPart, err)
|
||||
}
|
||||
@@ -3737,7 +3737,7 @@ func RepositoryToConfig(repo nip34.Repository) Nip34Config {
|
||||
}
|
||||
|
||||
func (localConfig Nip34Config) Validate() error {
|
||||
_, err := parsePubKey(localConfig.Owner)
|
||||
_, err := parsePubKey(localConfig.Owner, nostr.ZeroPK)
|
||||
if err != nil {
|
||||
return fmt.Errorf("owner pubkey '%s' is not valid: %w", localConfig.Owner, err)
|
||||
}
|
||||
@@ -3745,7 +3745,7 @@ func (localConfig Nip34Config) Validate() error {
|
||||
}
|
||||
|
||||
func (localConfig Nip34Config) ToRepository() nip34.Repository {
|
||||
owner, err := parsePubKey(localConfig.Owner)
|
||||
owner, err := parsePubKey(localConfig.Owner, nostr.ZeroPK)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -113,3 +113,5 @@ require (
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
|
||||
replace fiatjaf.com/nostr => ../nostrlib
|
||||
|
||||
+33
-1
@@ -21,6 +21,7 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip02"
|
||||
"fiatjaf.com/nostr/nip05"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"fiatjaf.com/nostr/schema"
|
||||
@@ -475,8 +476,13 @@ func askConfirmation(msg string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func parsePubKey(value string) (nostr.PubKey, error) {
|
||||
func parsePubKey(value string, from nostr.PubKey) (nostr.PubKey, error) {
|
||||
value = strings.TrimPrefix(value, "nostr:")
|
||||
|
||||
if strings.HasPrefix(value, "~") {
|
||||
return resolvePetnamePath(value, from)
|
||||
}
|
||||
|
||||
if nip05.IsValidIdentifier(value) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||
pp, err := nip05.QueryIdentifier(ctx, value)
|
||||
@@ -736,3 +742,29 @@ func combineFlags(flagSlices [][]cli.Flag, extraFlags ...cli.Flag) []cli.Flag {
|
||||
result = append(result, extraFlags...)
|
||||
return result
|
||||
}
|
||||
|
||||
// resolvePetnamePath resolves NIP-02 petname references like "~erin/david/frank" to a pubkey,
|
||||
// by walking the follow lists of each name in the chain. The first name is looked up in the
|
||||
// signer's own follow list (or its NIP-05 identifier, which can be mixed in, like
|
||||
// "~erin@names.com/david/frank"). See https://github.com/nostr-protocol/nips/blob/master/02.md.
|
||||
func resolvePetnamePath(value string, from nostr.PubKey) (nostr.PubKey, error) {
|
||||
if sys == nil {
|
||||
return nostr.ZeroPK, fmt.Errorf("can't resolve petnames before system initialization")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
pk, err := sys.ResolvePetnamePath(ctx, from, value)
|
||||
if err != nil {
|
||||
// InputToProfile intentionally hides NIP-05 lookup errors. Preserve
|
||||
// useful network errors instead of treating a failed lookup as petname.
|
||||
if parts, parseErr := nip02.ParsePetnamePath(value); parseErr == nil && len(parts) > 0 && nip05.IsValidIdentifier(parts[0]) {
|
||||
if _, nip05Err := nip05.QueryIdentifier(ctx, parts[0]); nip05Err != nil {
|
||||
return nostr.ZeroPK, nip05Err
|
||||
}
|
||||
}
|
||||
return nostr.ZeroPK, fmt.Errorf("failed to resolve \"%s\": %w", value, err)
|
||||
}
|
||||
return pk, nil
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
for i, keyhex := range result.Keys {
|
||||
pk32, err := parsePubKey(keyhex)
|
||||
pk32, err := parsePubKey(keyhex, nostr.ZeroPK)
|
||||
if err == nil { /* we'll try both the 02 and the 03 prefix versions */
|
||||
result.Keys[i] = pk32.Hex()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -22,7 +23,7 @@ var outboxCmd = &cli.Command{
|
||||
return fmt.Errorf("expected exactly one argument (pubkey)")
|
||||
}
|
||||
|
||||
pk, err := parsePubKey(c.Args().First())
|
||||
pk, err := parsePubKey(c.Args().First(), nostr.ZeroPK)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid public key '%s': %w", c.Args().First(), err)
|
||||
}
|
||||
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"fiatjaf.com/nostr/sdk"
|
||||
cache_memory "fiatjaf.com/nostr/sdk/cache/memory"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// makeFollowListEvent builds a signed kind-3 event for user with a set of ["p", pubkey, relay, petname] tags.
|
||||
func makeFollowListEvent(t *testing.T, sec nostr.SecretKey, ts nostr.Timestamp, refs ...sdk.ProfileRef) nostr.Event {
|
||||
t.Helper()
|
||||
|
||||
tags := make(nostr.Tags, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
tags = append(tags, nostr.Tag{"p", ref.Pubkey.Hex(), ref.Relay, ref.Petname})
|
||||
}
|
||||
|
||||
evt := nostr.Event{
|
||||
PubKey: sec.Public(),
|
||||
CreatedAt: ts,
|
||||
Kind: nostr.KindFollowList,
|
||||
Tags: tags,
|
||||
}
|
||||
require.NoError(t, evt.Sign(sec))
|
||||
return evt
|
||||
}
|
||||
|
||||
// setupPetnameSystem initializes a hermetic global sys by pre-seeding the follow list cache with
|
||||
// kind-3 events for the given identities, so no network or store access ever happens.
|
||||
func setupPetnameSystem(t *testing.T, lists map[nostr.SecretKey][]sdk.ProfileRef) {
|
||||
t.Helper()
|
||||
|
||||
sys = sdk.NewSystem()
|
||||
sys.FollowListCache = newSettledFollowListCache(t, lists)
|
||||
|
||||
t.Cleanup(func() {
|
||||
sys = nil
|
||||
rootSec = ""
|
||||
})
|
||||
}
|
||||
|
||||
// newSettledFollowListCache builds a follow list cache with the given entries already visible.
|
||||
// ristretto Set/SetWithTTL are asynchronous, so entries are read back until they stick.
|
||||
func newSettledFollowListCache(t *testing.T, lists map[nostr.SecretKey][]sdk.ProfileRef) *cache_memory.RistrettoCache[sdk.GenericList[nostr.PubKey, sdk.ProfileRef]] {
|
||||
t.Helper()
|
||||
cache := cache_memory.New[sdk.GenericList[nostr.PubKey, sdk.ProfileRef]](1000)
|
||||
for sk, refs := range lists {
|
||||
pubkey := sk.Public()
|
||||
v := sdk.GenericList[nostr.PubKey, sdk.ProfileRef]{PubKey: pubkey}
|
||||
if refs != nil {
|
||||
// nil refs means "no kind-3 event found at all"; empty refs means a tagless event
|
||||
evt := makeFollowListEvent(t, sk, 1000, refs...)
|
||||
v.Event = &evt
|
||||
v.Items = refs
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for {
|
||||
if cache.SetWithTTL(pubkey, v, time.Hour) {
|
||||
if _, ok := cache.Get(pubkey); ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
require.Less(t, time.Now(), deadline, "cache entry did not settle")
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
func keyFromSeed(t *testing.T, seed byte) nostr.SecretKey {
|
||||
t.Helper()
|
||||
sk := nostr.SecretKey{}
|
||||
for i := range sk {
|
||||
sk[i] = seed + byte(i)
|
||||
}
|
||||
return sk
|
||||
}
|
||||
|
||||
func TestResolvePetnamePathChain(t *testing.T) {
|
||||
me := keyFromSeed(t, 1)
|
||||
erin := keyFromSeed(t, 2)
|
||||
david := keyFromSeed(t, 3)
|
||||
frank := keyFromSeed(t, 4)
|
||||
|
||||
setupPetnameSystem(t, map[nostr.SecretKey][]sdk.ProfileRef{
|
||||
me: {{Pubkey: erin.Public(), Petname: "erin"}},
|
||||
erin: {{Pubkey: david.Public(), Petname: "david"}},
|
||||
david: {{Pubkey: frank.Public(), Petname: "frank"}},
|
||||
})
|
||||
rootSec = me.Hex()
|
||||
|
||||
pk, err := resolvePetnamePath("~erin/david/frank")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, frank.Public(), pk)
|
||||
}
|
||||
|
||||
func TestResolvePetnamePathSingleHop(t *testing.T) {
|
||||
me := keyFromSeed(t, 1)
|
||||
erin := keyFromSeed(t, 2)
|
||||
|
||||
setupPetnameSystem(t, map[nostr.SecretKey][]sdk.ProfileRef{
|
||||
me: {{Pubkey: erin.Public(), Petname: "erin"}},
|
||||
})
|
||||
rootSec = me.Hex()
|
||||
|
||||
pk, err := resolvePetnamePath("~erin")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, erin.Public(), pk)
|
||||
}
|
||||
|
||||
func TestResolvePetnamePathCurrentUser(t *testing.T) {
|
||||
me := keyFromSeed(t, 1)
|
||||
erin := keyFromSeed(t, 2)
|
||||
|
||||
setupPetnameSystem(t, map[nostr.SecretKey][]sdk.ProfileRef{
|
||||
me: {{Pubkey: erin.Public(), Petname: "erin"}},
|
||||
})
|
||||
rootSec = me.Hex()
|
||||
|
||||
pk, err := resolvePetnamePath("~/erin")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, erin.Public(), pk)
|
||||
}
|
||||
|
||||
func TestResolvePetnamePathDirectRootWithoutIdentity(t *testing.T) {
|
||||
me := keyFromSeed(t, 1)
|
||||
erin := keyFromSeed(t, 2)
|
||||
|
||||
setupPetnameSystem(t, map[nostr.SecretKey][]sdk.ProfileRef{
|
||||
me: {{Pubkey: erin.Public(), Petname: "erin"}},
|
||||
})
|
||||
|
||||
pk, err := resolvePetnamePath("~" + nip19.EncodeNpub(me.Public()) + "/erin")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, erin.Public(), pk)
|
||||
}
|
||||
|
||||
func TestResolvePetnamePathNip05Root(t *testing.T) {
|
||||
erin := keyFromSeed(t, 2)
|
||||
david := keyFromSeed(t, 3)
|
||||
|
||||
setupPetnameSystem(t, map[nostr.SecretKey][]sdk.ProfileRef{
|
||||
erin: {{Pubkey: david.Public(), Petname: "david"}},
|
||||
})
|
||||
|
||||
// the first segment must be routed to nip05 (which will fail to connect here, as nothing
|
||||
// listens on 127.0.0.1:443) instead of being treated as a petname
|
||||
_, err := resolvePetnamePath("~erin@127.0.0.1/david")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "request failed")
|
||||
require.NotContains(t, err.Error(), "follow list")
|
||||
}
|
||||
|
||||
func TestResolvePetnamePathErrors(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"}},
|
||||
erin: {{Pubkey: david.Public(), Petname: "david"}},
|
||||
})
|
||||
rootSec = me.Hex()
|
||||
|
||||
// unknown petname at root
|
||||
_, err := resolvePetnamePath("~carol")
|
||||
require.ErrorContains(t, err, "no one in your follow list")
|
||||
|
||||
// unknown petname mid-chain
|
||||
_, err = resolvePetnamePath("~erin/carol")
|
||||
require.ErrorContains(t, err, "doesn't follow anyone named")
|
||||
|
||||
// empty reference
|
||||
_, err = resolvePetnamePath("~")
|
||||
require.Error(t, err)
|
||||
|
||||
// empty segment
|
||||
_, err = resolvePetnamePath("~erin//frank")
|
||||
require.Error(t, err)
|
||||
|
||||
// invalid petname characters
|
||||
_, err = resolvePetnamePath("~er\tin")
|
||||
require.Error(t, err)
|
||||
|
||||
// no identity
|
||||
rootSec = ""
|
||||
_, err = resolvePetnamePath("~erin")
|
||||
require.ErrorContains(t, err, "no secret key")
|
||||
}
|
||||
|
||||
func TestResolvePetnameMissingFollowList(t *testing.T) {
|
||||
me := keyFromSeed(t, 1)
|
||||
erin := keyFromSeed(t, 2)
|
||||
|
||||
// erin has a cached follow list record with no event, i.e. her kind-3 was never found
|
||||
sys = sdk.NewSystem()
|
||||
sys.FollowListCache = newSettledFollowListCache(t, map[nostr.SecretKey][]sdk.ProfileRef{
|
||||
me: {{Pubkey: erin.Public(), Petname: "erin"}},
|
||||
erin: nil,
|
||||
})
|
||||
t.Cleanup(func() { sys = nil; rootSec = "" })
|
||||
rootSec = me.Hex()
|
||||
|
||||
_, err := resolvePetnamePath("~erin/frank")
|
||||
require.ErrorContains(t, err, "couldn't get follow list")
|
||||
}
|
||||
|
||||
func TestParsePubKeyUnaffectedInputs(t *testing.T) {
|
||||
me := keyFromSeed(t, 1)
|
||||
erin := keyFromSeed(t, 2)
|
||||
|
||||
setupPetnameSystem(t, map[nostr.SecretKey][]sdk.ProfileRef{
|
||||
me: {{Pubkey: erin.Public(), Petname: "erin"}},
|
||||
})
|
||||
rootSec = me.Hex()
|
||||
|
||||
// plain npub keeps working through parsePubKey
|
||||
pk, err := parsePubKey(nip19.EncodeNpub(erin.Public()), nostr.ZeroPK)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, erin.Public(), pk)
|
||||
|
||||
// hex keeps working
|
||||
pk, err = parsePubKey(erin.Public().Hex(), nostr.ZeroPK)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, erin.Public(), pk)
|
||||
|
||||
// without a system initialized, petname resolution fails cleanly
|
||||
sys = nil
|
||||
_, err = parsePubKey("~erin", nostr.ZeroPK)
|
||||
require.Error(t, err)
|
||||
require.True(t, strings.Contains(err.Error(), "petname") || strings.Contains(err.Error(), "system"))
|
||||
}
|
||||
+2
-1
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
stdjson "encoding/json"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -39,7 +40,7 @@ example usage:
|
||||
ArgsUsage: "[pubkey]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for pubkeyInput := range getStdinLinesOrArguments(c.Args()) {
|
||||
pk, err := parsePubKey(pubkeyInput)
|
||||
pk, err := parsePubKey(pubkeyInput, nostr.ZeroPK)
|
||||
if err != nil {
|
||||
ctx = lineProcessingError(ctx, "invalid pubkey '%s': %s", pubkeyInput, err)
|
||||
continue
|
||||
|
||||
@@ -295,7 +295,7 @@ func performReq(
|
||||
pTags := append([]string{}, filter.Tags["p"]...)
|
||||
pTags = append(pTags, filter.Tags["P"]...)
|
||||
for _, value := range pTags {
|
||||
pubkey, err := parsePubKey(value)
|
||||
pubkey, err := parsePubKey(value, nostr.ZeroPK)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ func buildSpellReq(ctx context.Context, c *cli.Command, tags nostr.Tags) (nostr.
|
||||
|
||||
getMe := func() (nostr.PubKey, error) {
|
||||
if !c.IsSet("sec") && !c.IsSet("prompt-sec") && c.IsSet("pub") {
|
||||
return parsePubKey(c.String("pub"))
|
||||
return parsePubKey(c.String("pub"), nostr.ZeroPK)
|
||||
}
|
||||
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
|
||||
Reference in New Issue
Block a user