From cc4f3a0bf56df9813c79b775aded9c03bc72c3b9 Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:40:31 +0100 Subject: [PATCH 1/8] feat(bunker): introduce command interface with QR code and help commands - Add interactive command interface for the bunker. - Implement `help`, `info`, `qr`, and `exit` commands for user convenience. - Display QR code on demand using `qr` command. - Ensure proper command handling with locking mechanism and safe shutdown using `exit`. --- bunker.go | 169 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 119 insertions(+), 50 deletions(-) diff --git a/bunker.go b/bunker.go index 850e2f9..3ae9908 100644 --- a/bunker.go +++ b/bunker.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "bytes" "context" "encoding/hex" @@ -229,6 +230,31 @@ var bunker = &cli.Command{ // static information pubkey := sec.Public() npub := nip19.EncodeNpub(pubkey) + printLock := sync.Mutex{} + exitChan := make(chan bool, 1) + + // == SUB COMMANDS == + + // printHelp displays available commands for the bunker interface + printHelp := func() { + log("%s\n", color.CyanString("Available Commands:")) + log(" %s - Show this help message\n", color.GreenString("help, h, ?")) + log(" %s - Display current bunker information\n", color.GreenString("info, i")) + log(" %s - Generate and display QR code for the bunker URI\n", color.GreenString("qr")) + log(" %s - Shutdown the bunker\n", color.GreenString("exit, quit, q")) + log("\n") + } + + // Create a function to print QR code on demand + printQR := func() { + qs.Set("secret", newSecret) + bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey.Hex(), qs.Encode()) + printLock.Lock() + log("\nQR Code for bunker URI:\n") + qrterminal.Generate(bunkerURI, qrterminal.L, os.Stdout) + log("\n\n") + printLock.Unlock() + } // this function will be called every now and then printBunkerInfo := func() { @@ -301,11 +327,30 @@ var bunker = &cli.Command{ // print QR code if requested if c.Bool("qrcode") { - log("QR Code for bunker URI:\n") - qrterminal.Generate(bunkerURI, qrterminal.L, os.Stdout) - log("\n\n") + printQR() } } + + // handleBunkerCommand processes user commands in the bunker interface + handleBunkerCommand := func(command string) { + switch strings.ToLower(command) { + case "help", "h", "?": + printHelp() + case "info", "i": + printBunkerInfo() + case "qr": + printQR() + case "exit", "quit", "q": + log("Exit command received.\n") + exitChan <- true + case "": + // Ignore empty commands + default: + log("Unknown command: %s. Type 'help' for available commands.\n", command) + } + } + + // Print initial bunker information printBunkerInfo() // subscribe to relays @@ -320,7 +365,6 @@ var bunker = &cli.Command{ signer := nip46.NewStaticKeySigner(sec) handlerWg := sync.WaitGroup{} - printLock := sync.Mutex{} // just a gimmick var cancelPreviousBunkerInfoPrint context.CancelFunc @@ -348,56 +392,81 @@ var bunker = &cli.Command{ return slices.Contains(config.AuthorizedKeys, from) || slices.Contains(authorizedSecrets, secret) } - for ie := range events { - cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks + // Start command input handler in a separate goroutine + go func() { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + command := strings.TrimSpace(scanner.Text()) + handleBunkerCommand(command) + } + if err := scanner.Err(); err != nil { + log("error reading command: %v\n", err) + } + }() - // handle the NIP-46 request event - req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event) - if err != nil { - log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error()) + // Print initial command help + log("%s\nType 'help' for available commands or 'exit' to quit.\n%s\n", + color.CyanString("--------------- Bunker Command Interface ---------------"), + color.CyanString("--------------------------------------------------------")) + + for { + // Check if exit was requested first + select { + case <-exitChan: + log("Shutting down bunker...\n") + return nil + case ie := <-events: + cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks + + // handle the NIP-46 request event + req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event) + if err != nil { + log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error()) + continue + } + + jreq, _ := json.MarshalIndent(req, "", " ") + log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey.Hex()), string(jreq)) + jresp, _ := json.MarshalIndent(resp, "", " ") + log("~ responding with %s\n", string(jresp)) + + handlerWg.Add(len(relayURLs)) + for _, relayURL := range relayURLs { + go func(relayURL string) { + if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil { + err := relay.Publish(ctx, eventResponse) + printLock.Lock() + if err == nil { + log("* sent response through %s\n", relay.URL) + } else { + log("* failed to send response: %s\n", err) + } + printLock.Unlock() + handlerWg.Done() + } + }(relayURL) + } + handlerWg.Wait() + + // just after handling one request we trigger this + go func() { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + cancelPreviousBunkerInfoPrint = cancel + // the idea is that we will print the bunker URL again so it is easier to copy-paste by users + // but we will only do if the bunker is inactive for more than 5 minutes + select { + case <-ctx.Done(): + case <-time.After(time.Minute * 5): + log("\n") + printBunkerInfo() + } + }() + case <-time.After(100 * time.Millisecond): + // Continue to check for exit signal even when no events continue } - - jreq, _ := json.MarshalIndent(req, "", " ") - log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey.Hex()), string(jreq)) - jresp, _ := json.MarshalIndent(resp, "", " ") - log("~ responding with %s\n", string(jresp)) - - handlerWg.Add(len(relayURLs)) - for _, relayURL := range relayURLs { - go func(relayURL string) { - if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil { - err := relay.Publish(ctx, eventResponse) - printLock.Lock() - if err == nil { - log("* sent response through %s\n", relay.URL) - } else { - log("* failed to send response: %s\n", err) - } - printLock.Unlock() - handlerWg.Done() - } - }(relayURL) - } - handlerWg.Wait() - - // just after handling one request we trigger this - go func() { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - cancelPreviousBunkerInfoPrint = cancel - // the idea is that we will print the bunker URL again so it is easier to copy-paste by users - // but we will only do if the bunker is inactive for more than 5 minutes - select { - case <-ctx.Done(): - case <-time.After(time.Minute * 5): - log("\n") - printBunkerInfo() - } - }() } - - return nil }, Commands: []*cli.Command{ { From ea316941727c4ca80d0c6fb9af9f78fa799fa8e6 Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:45:40 +0100 Subject: [PATCH 2/8] refactor(bunker): remove unused NostrConnect `connect` command --- bunker.go | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/bunker.go b/bunker.go index 3ae9908..fd8dff8 100644 --- a/bunker.go +++ b/bunker.go @@ -468,27 +468,6 @@ var bunker = &cli.Command{ } } }, - Commands: []*cli.Command{ - { - Name: "connect", - Usage: "use the client-initiated NostrConnect flow of NIP46", - ArgsUsage: "", - Action: func(ctx context.Context, c *cli.Command) error { - if c.Args().Len() != 1 { - return fmt.Errorf("must be called with a nostrconnect://... uri") - } - - uri, err := url.Parse(c.Args().First()) - if err != nil || uri.Scheme != "nostrconnect" { - return fmt.Errorf("invalid uri") - } - - // TODO - - return fmt.Errorf("this is not implemented yet") - }, - }, - }, } type plainOrEncryptedKey struct { From a4d4639e601f47d94d2db72c22c2d1c517cc4b81 Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Fri, 4 Jul 2025 17:51:20 +0100 Subject: [PATCH 3/8] feat(bunker): add NostrConnect `connect` command support to bunker - Implement `connect` command to enable processing `nostrconnect://` URIs. - Update configuration dynamically with new relays and authorized keys. --- bunker.go | 207 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 192 insertions(+), 15 deletions(-) diff --git a/bunker.go b/bunker.go index fd8dff8..adc232f 100644 --- a/bunker.go +++ b/bunker.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "encoding/hex" + "fiatjaf.com/nostr/nip44" "fmt" "net/url" "os" @@ -230,10 +231,23 @@ var bunker = &cli.Command{ // static information pubkey := sec.Public() npub := nip19.EncodeNpub(pubkey) + signer := nip46.NewStaticKeySigner(sec) printLock := sync.Mutex{} exitChan := make(chan bool, 1) - // == SUB COMMANDS == + // subscribe to relays + events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{ + Kinds: []nostr.Kind{nostr.KindNostrConnect}, + Tags: nostr.TagMap{"p": []string{pubkey.Hex()}}, + Since: nostr.Now(), + LimitZero: true, + }, nostr.SubscriptionOptions{ + Label: "nak-bunker", + }) + + handlerWg := sync.WaitGroup{} + + // == SUBCOMMANDS == // printHelp displays available commands for the bunker interface printHelp := func() { @@ -241,6 +255,7 @@ var bunker = &cli.Command{ log(" %s - Show this help message\n", color.GreenString("help, h, ?")) log(" %s - Display current bunker information\n", color.GreenString("info, i")) log(" %s - Generate and display QR code for the bunker URI\n", color.GreenString("qr")) + log(" %s - Connect to a remote client using nostrconnect:// URI\n", color.GreenString("connect, c ")) log(" %s - Shutdown the bunker\n", color.GreenString("exit, quit, q")) log("\n") } @@ -331,15 +346,188 @@ var bunker = &cli.Command{ } } + // handleConnect processes nostrconnect:// URIs for direct connection flow + handleConnect := func(connectURI string) { + if !strings.HasPrefix(connectURI, "nostrconnect://") { + log("Error: URI must start with nostrconnect://\n") + return + } + + // Parse the nostrconnect URI + u, err := url.Parse(connectURI) + if err != nil { + log("Error: Invalid nostrconnect URI: %v\n", err) + return + } + + // Extract client pubkey from the URI + clientPubkeyHex := u.Host + if clientPubkeyHex == "" { + log("Error: Missing client pubkey in URI\n") + return + } + + clientPubkey, err := nostr.PubKeyFromHex(clientPubkeyHex) + if err != nil { + log("Error: Invalid client pubkey: %v\n", err) + return + } + + // Extract relays from query parameters + queryParams := u.Query() + newRelayURLs := queryParams["relay"] + if len(newRelayURLs) == 0 { + log("Error: No relays specified in URI\n") + return + } + + // Extract secret from query parameters (optional) + var secret string + secrets := queryParams["secret"] + if len(secrets) > 0 { + secret = secrets[0] + } else { + // log error and return if no secret is provided + log("Error: No secret provided in URI\n") + return + } + + log("Parsed nostrconnect URI:\n") + log(" Client pubkey: %s\n", color.CyanString(clientPubkey.Hex())) + log(" Relays: %v\n", newRelayURLs) + log(" Secret: %s\n", color.YellowString(secret)) + + // Normalize relay URLs + for i, relayURL := range newRelayURLs { + newRelayURLs[i] = nostr.NormalizeURL(relayURL) + } + + // Update relays in config (add new ones) + relaysAdded := false + for _, relayURL := range newRelayURLs { + if !slices.Contains(config.Relays, relayURL) { + config.Relays = append(config.Relays, relayURL) + log("Added new relay: %s\n", color.MagentaString(relayURL)) + relaysAdded = true + } + } + + // Add client key to authorized keys + keyAdded := false + if !slices.Contains(config.AuthorizedKeys, clientPubkey) { + config.AuthorizedKeys = append(config.AuthorizedKeys, clientPubkey) + log("Added client key to authorized keys: %s\n", color.GreenString(clientPubkey.Hex())) + keyAdded = true + } + + // Persist config if needed and changes were made + if persist != nil && (relaysAdded || keyAdded) { + persist() + log(color.GreenString("Configuration saved\n")) + } + + // Connect to new relays if any were added + if relaysAdded { + newRelays := connectToAllRelays(ctx, c, newRelayURLs, nil, nostr.PoolOptions{}) + log("Connected to %d new relay(s)\n", len(newRelays)) + + // Update the relay URLs list with successfully connected relays + for _, relay := range newRelays { + if !slices.Contains(relayURLs, relay.URL) { + relayURLs = append(relayURLs, relay.URL) + } + } + log("Updated subscription to listen on %d relay(s)\n", len(relayURLs)) + } + + responsePayload := nip46.Response{ + ID: secret, + Result: "ack", + } + + // Marshal the response payload to JSON + responseJSON, err := json.MarshalIndent(responsePayload, "", " ") + if err != nil { + log("Error: Failed to marshal response payload: %v\n", err) + return + } + + log("Sending connect response with:\n%s\n", string(responseJSON)) + + // Encrypt the response using NIP-44 + conversationKey, err := nip44.GenerateConversationKey(clientPubkey, sec) + if err != nil { + log("Error: Failed to generate conversation key: %v\n", err) + return + } + encryptedContent, err := nip44.Encrypt(string(responseJSON), conversationKey) + if err != nil { + log("Error: Failed to encrypt response content: %v\n", err) + return + } + + // Create the kind 24133 event + eventResponse := nostr.Event{ + Kind: nostr.KindNostrConnect, + PubKey: pubkey, + Content: encryptedContent, + Tags: nostr.Tags{{"p", clientPubkey.Hex()}}, + CreatedAt: nostr.Now(), + } + + // Sign the event with the signer + if err := eventResponse.Sign(sec); err != nil { + log("Error: Failed to sign connect response: %v\n", err) + return + } + + // Create a fake Kind 24133 connect request to process through the signer + targetRelays := newRelayURLs + if len(targetRelays) == 0 { + targetRelays = relayURLs + } + + log("Sending connect response...\n") + successCount := 0 + for _, relayURL := range targetRelays { + if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil { + err := relay.Publish(ctx, eventResponse) + if err != nil { + log("Failed to publish to %s: %v\n", relayURL, err) + } else { + log("Published connect response to %s\n", color.GreenString(relayURL)) + successCount++ + } + } + } + + if successCount == 0 { + log("Error: Failed to publish connect response to any relay\n") + } else { + log(color.GreenString("\nConnect response sent successfully to %d relay(s)!\n"), successCount) + } + } + // handleBunkerCommand processes user commands in the bunker interface handleBunkerCommand := func(command string) { - switch strings.ToLower(command) { + parts := strings.Fields(command) + if len(parts) == 0 { + return + } + + switch strings.ToLower(parts[0]) { case "help", "h", "?": printHelp() case "info", "i": printBunkerInfo() case "qr": printQR() + case "connect", "c": + if len(parts) < 2 { + log("Usage: connect \n") + return + } + handleConnect(parts[1]) case "exit", "quit", "q": log("Exit command received.\n") exitChan <- true @@ -350,22 +538,11 @@ var bunker = &cli.Command{ } } + // == END OF SUBCOMMANDS == + // Print initial bunker information printBunkerInfo() - // subscribe to relays - events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{ - Kinds: []nostr.Kind{nostr.KindNostrConnect}, - Tags: nostr.TagMap{"p": []string{pubkey.Hex()}}, - Since: nostr.Now(), - LimitZero: true, - }, nostr.SubscriptionOptions{ - Label: "nak-bunker", - }) - - signer := nip46.NewStaticKeySigner(sec) - handlerWg := sync.WaitGroup{} - // just a gimmick var cancelPreviousBunkerInfoPrint context.CancelFunc _, cancel := context.WithCancel(ctx) From 32ca7ecb60db1fdd7628600300d9d9310652834d Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:06:46 +0100 Subject: [PATCH 4/8] refactor(bunker): manage subscriptions dynamically for relay updates - Refactor subscription management to allow dynamic relay updates. - Introduce cancelable context and lock mechanism to safely recreate subscriptions. - Automatically update subscriptions when relay list changes. --- bunker.go | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/bunker.go b/bunker.go index adc232f..9f7303a 100644 --- a/bunker.go +++ b/bunker.go @@ -235,16 +235,38 @@ var bunker = &cli.Command{ printLock := sync.Mutex{} exitChan := make(chan bool, 1) - // subscribe to relays - events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{ - Kinds: []nostr.Kind{nostr.KindNostrConnect}, - Tags: nostr.TagMap{"p": []string{pubkey.Hex()}}, - Since: nostr.Now(), - LimitZero: true, - }, nostr.SubscriptionOptions{ - Label: "nak-bunker", - }) + // subscription management + var events chan nostr.RelayEvent + var cancelSubscription context.CancelFunc + subscriptionMutex := sync.Mutex{} + // Function to create/recreate subscription + updateSubscription := func() { + subscriptionMutex.Lock() + defer subscriptionMutex.Unlock() + + // Cancel existing subscription if it exists + if cancelSubscription != nil { + cancelSubscription() + } + + // Create new context for the subscription + subCtx, cancel := context.WithCancel(ctx) + cancelSubscription = cancel + + // Create new subscription with current relay list + events = sys.Pool.SubscribeMany(subCtx, relayURLs, nostr.Filter{ + Kinds: []nostr.Kind{nostr.KindNostrConnect}, + Tags: nostr.TagMap{"p": []string{pubkey.Hex()}}, + Since: nostr.Now(), + LimitZero: true, + }, nostr.SubscriptionOptions{ + Label: "nak-bunker", + }) + } + + // Initial subscription to relays + updateSubscription() handlerWg := sync.WaitGroup{} // == SUBCOMMANDS == @@ -438,6 +460,10 @@ var bunker = &cli.Command{ } } log("Updated subscription to listen on %d relay(s)\n", len(relayURLs)) + + // Cancel and recreate subscription with updated relay list + updateSubscription() + log("Subscription updated to include new relays\n") } responsePayload := nip46.Response{ From c8844f7d0fed941a07fa84b1b73959ed90e1c81b Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:35:06 +0100 Subject: [PATCH 5/8] feat(bunker): publish bunker connect responses concurrently - Introduce goroutines to handle concurrent publishing to target relays. - Use `sync/atomic` for thread-safe success counter tracking - Add `WaitGroup` to ensure all goroutines complete before proceeding. --- bunker.go | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/bunker.go b/bunker.go index 9f7303a..42f808d 100644 --- a/bunker.go +++ b/bunker.go @@ -13,6 +13,7 @@ import ( "slices" "strings" "sync" + "sync/atomic" "time" "fiatjaf.com/nostr" @@ -466,6 +467,7 @@ var bunker = &cli.Command{ log("Subscription updated to include new relays\n") } + // Prepare the response payload responsePayload := nip46.Response{ ID: secret, Result: "ack", @@ -507,30 +509,36 @@ var bunker = &cli.Command{ return } - // Create a fake Kind 24133 connect request to process through the signer targetRelays := newRelayURLs if len(targetRelays) == 0 { targetRelays = relayURLs } log("Sending connect response...\n") - successCount := 0 + successCount := atomic.Uint32{} + handlerWg.Add(len(targetRelays)) for _, relayURL := range targetRelays { - if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil { - err := relay.Publish(ctx, eventResponse) - if err != nil { - log("Failed to publish to %s: %v\n", relayURL, err) - } else { - log("Published connect response to %s\n", color.GreenString(relayURL)) - successCount++ + go func(relayURL string) { + if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil { + err := relay.Publish(ctx, eventResponse) + printLock.Lock() + if err != nil { + log("Failed to publish to %s: %v\n", relayURL, err) + } else { + log("Published connect response to %s\n", color.GreenString(relayURL)) + successCount.Add(1) + } + printLock.Unlock() + handlerWg.Done() } - } + }(relayURL) } + handlerWg.Wait() - if successCount == 0 { + if successCount.Load() == 0 { log("Error: Failed to publish connect response to any relay\n") } else { - log(color.GreenString("\nConnect response sent successfully to %d relay(s)!\n"), successCount) + log(color.GreenString("\nConnect response sent successfully to %d relay(s)!\n"), successCount.Load()) } } From 660a2063594c6562c504ab8730077016de0897c6 Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:38:26 +0100 Subject: [PATCH 6/8] feat(bunker): add bunker info print after direct connect response - Call `printBunkerInfo` to display bunker details after sending connect response --- bunker.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bunker.go b/bunker.go index 42f808d..7eef1b8 100644 --- a/bunker.go +++ b/bunker.go @@ -540,6 +540,8 @@ var bunker = &cli.Command{ } else { log(color.GreenString("\nConnect response sent successfully to %d relay(s)!\n"), successCount.Load()) } + + printBunkerInfo() } // handleBunkerCommand processes user commands in the bunker interface From 3bdb1f65cf592b63938d84239fa18b8c6f6af6da Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:50:07 +0100 Subject: [PATCH 7/8] feat(bunker): delay bunker info print after direct connect response - Introduce a 3-second delay before calling `printBunkerInfo` --- bunker.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bunker.go b/bunker.go index 7eef1b8..6701568 100644 --- a/bunker.go +++ b/bunker.go @@ -541,7 +541,11 @@ var bunker = &cli.Command{ log(color.GreenString("\nConnect response sent successfully to %d relay(s)!\n"), successCount.Load()) } - printBunkerInfo() + // print bunker info again after this + go func() { + time.Sleep(3 * time.Second) + printBunkerInfo() + }() } // handleBunkerCommand processes user commands in the bunker interface From f78663909c6faa38d8a0307df8688880832b1ea9 Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Fri, 4 Jul 2025 19:05:43 +0100 Subject: [PATCH 8/8] docs(readme): add bunker subcommands documentation - Document new bunker subcommands, including `help`, `info`, `qr`, and `connect`. - Update examples for displaying bunker QR codes and usage instructions. --- README.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7cade1f..75da72a 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,31 @@ listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]: bunker: bunker://f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Fnos.lol&relay=wss%3A%2F%2Frelay.nsecbunker.com&secret=XuuiMbcLwuwL ``` -you can also display a QR code for the bunker URI by adding the `--qrcode` flag: +#### Bunker subcommands + +Bunker has a few subcommands that you can use to manage it, type `help` to see them all: +```shell +~> ./nak bunker relay.nsec.app +wss://relay.nsec.app... ok. +listening at [wss://relay.nsec.app]: + pubkey: f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a + npub: npub17kv3rdtpcd7fpvq7newz24eswwqgxhyr8xt4daxk9kqkwgn7gg9q4gy8vf + to restart: nak bunker relay.nsec.app + bunker: bunker://f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a?relay=wss%3A%2F%2Frelay.nsec.app&secret=cAMoUOddVMla + +--------------- Bunker Command Interface --------------- +Type 'help' for available commands or 'exit' to quit. +-------------------------------------------------------- +help +Available Commands: + help, h, ? - Show this help message + info, i - Display current bunker information + qr - Generate and display QR code for the bunker URI + connect, c - Connect to a remote client using nostrconnect:// URI + exit, quit, q - Shutdown the bunker +``` + +You can also display a QR code for the bunker URI by adding the `--qrcode` flag: ```shell ~> nak bunker --qrcode --sec ncryptsec1... relay.damus.io