NAME

Net::Nostr::RemoteSigning - NIP-46 Nostr Remote Signing

SYNOPSIS

use Net::Nostr::RemoteSigning;

my $remote_signer_pubkey = 'fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52';
my $client_pubkey        = 'eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86';

# Parse a bunker URI (remote-signer initiated)
my $conn = Net::Nostr::RemoteSigning->parse_bunker_uri(
    "bunker://${remote_signer_pubkey}?relay=wss%3A%2F%2Frelay.example.com&secret=mysecret"
);
say $conn->remote_signer_pubkey;
say $conn->relays->[0];

# Parse a nostrconnect URI (client initiated)
my $nc = Net::Nostr::RemoteSigning->parse_nostrconnect_uri(
    "nostrconnect://${client_pubkey}?relay=wss%3A%2F%2Frelay.example.com&secret=0s8j2djs&name=My+Client"
);
say $nc->client_pubkey;
say $nc->secret;

# Build a request payload
my $payload = Net::Nostr::RemoteSigning->request(
    id     => 'req-1',
    method => 'sign_event',
    params => ['{}'],
);

# Build a request event (kind 24133)
my $event = Net::Nostr::RemoteSigning->request_event(
    id                   => 'req-1',
    method               => 'sign_event',
    params               => ['{}'],
    pubkey               => $client_pubkey,
    remote_signer_pubkey => $remote_signer_pubkey,
);

# Parse a decrypted response
my $json = JSON->new->utf8->encode({ id => 'req-1', result => 'pong' });
my $resp = Net::Nostr::RemoteSigning->parse_response($json);
if ($resp->is_auth_challenge) {
    # Display $resp->auth_url to user
} elsif ($resp->is_error) {
    warn $resp->error;
} else {
    say $resp->result;
}

# Parse permissions
my @perms = Net::Nostr::RemoteSigning->parse_permissions(
    'nip44_encrypt,sign_event:4'
);

DESCRIPTION

Implements NIP-46 Nostr Remote Signing, a protocol for 2-way communication between a remote signer (bunker) and a Nostr client. The remote signer holds the user's private keys and signs events on behalf of the client, minimizing key exposure.

Both request and response events use kind 24133. The content field is encrypted using NIP-44. This module handles the payload structure and event creation; the caller is responsible for encrypting/decrypting the content.

Connection Flow

There are two ways to initiate a connection:

  • Remote-signer initiated - The signer provides a bunker:// URI. The client sends a connect request to the signer via the specified relays.

  • Client initiated - The client provides a nostrconnect:// URI. The signer sends a connect response back to the client.

Commands

connect, sign_event, ping, get_public_key, nip04_encrypt, nip04_decrypt, nip44_encrypt, nip44_decrypt, switch_relays.

CLASS METHODS

parse_bunker_uri

my $conn = Net::Nostr::RemoteSigning->parse_bunker_uri($uri_string);

Parses a bunker:// connection URI. Returns a "BunkerConnection" object. Croaks if the URI is malformed or missing the required relay parameter. The secret parameter is optional.

create_bunker_uri

my $uri = Net::Nostr::RemoteSigning->create_bunker_uri(
    remote_signer_pubkey => $hex_pubkey,       # required
    relay                => $url_or_arrayref,  # required
    secret               => $secret_string,    # optional
);

Creates a bunker:// URI string.

parse_nostrconnect_uri

my $nc = Net::Nostr::RemoteSigning->parse_nostrconnect_uri($uri_string);

Parses a nostrconnect:// connection URI. Returns a "NostrConnect" object. Croaks if the URI is malformed or missing required parameters (relay, secret).

create_nostrconnect_uri

my $uri = Net::Nostr::RemoteSigning->create_nostrconnect_uri(
    client_pubkey => $hex_pubkey,              # required
    relay         => $url_or_arrayref,         # required
    secret        => $secret_string,           # required
    perms         => 'nip44_encrypt,sign_event:4',  # optional
    name          => 'My Client',              # optional
    url           => 'https://app.example.com',     # optional
    image         => 'https://app.example.com/i.png', # optional
);

Creates a nostrconnect:// URI string.

request

my $json = Net::Nostr::RemoteSigning->request(
    id     => 'req-1',    # optional, auto-generated if omitted
    method => 'ping',
    params => [],
);

Builds a JSON-encoded request payload. Croaks if method or params is missing.

request_event

my $event = Net::Nostr::RemoteSigning->request_event(
    id                   => 'req-1',
    method               => 'sign_event',
    params               => [$event_json],
    pubkey               => $client_pubkey,
    remote_signer_pubkey => $remote_signer_pubkey,
);

Creates a kind 24133 request Net::Nostr::Event with the JSON payload as unencrypted content and a p tag with the remote signer's pubkey. Croaks if pubkey is missing or not 64-char lowercase hex.

response

my $json = Net::Nostr::RemoteSigning->response(
    id     => 'req-1',
    result => 'pong',
);

Builds a JSON-encoded response payload. Croaks if id is missing.

response_event

my $event = Net::Nostr::RemoteSigning->response_event(
    id            => 'req-1',
    result        => 'pong',
    pubkey        => $remote_signer_pubkey,
    client_pubkey => $client_pubkey,
);

Creates a kind 24133 response Net::Nostr::Event with a p tag pointing to the client's pubkey. Croaks if pubkey is missing or not 64-char lowercase hex.

parse_request

my $req = Net::Nostr::RemoteSigning->parse_request($json);

Parses a decrypted JSON request payload. Returns a "Request" object. Croaks if id, method, or params is missing, or if params is not an arrayref.

parse_response

my $resp = Net::Nostr::RemoteSigning->parse_response($json);

Parses a decrypted JSON response payload. Returns a "Response" object. Croaks if id is missing.

parse_permissions

my @perms = Net::Nostr::RemoteSigning->parse_permissions($perms_string);

Parses a comma-separated permission string (e.g. 'nip44_encrypt,sign_event:4'). Returns a list of hashrefs with method and optional param keys.

validate_request

Net::Nostr::RemoteSigning->validate_request($event);

Validates that a request event is kind 24133 and has a p tag. Croaks on failure.

validate_response

Net::Nostr::RemoteSigning->validate_response($event);

Validates that a response event is kind 24133. Croaks on failure.

validate_connect_response

Net::Nostr::RemoteSigning->validate_connect_response($resp, $expected_secret);

Validates that a connect response contains the expected secret. If no expected secret is provided, accepts any response (for bunker-initiated connections where the result is "ack"). Croaks if the secret does not match.

parse_switch_relays

my $relays = Net::Nostr::RemoteSigning->parse_switch_relays($result_string);

Parses a switch_relays response result. Returns an arrayref of relay URLs, or undef if the result is null (indicating no relay change needed).

parse_nip05_metadata

my $meta = Net::Nostr::RemoteSigning->parse_nip05_metadata($json);

Parses a NIP-05 .well-known/nostr.json response containing NIP-46 metadata. Returns a "Nip05Metadata" object. Croaks if the response does not contain a names._ field.

parse_discovery_event

my $disc = Net::Nostr::RemoteSigning->parse_discovery_event($event);

Parses a NIP-89 kind 31990 discovery event. Returns a "Discovery" object. Croaks if the event is not kind 31990 or lacks a k tag with value 24133.

discovery_event

my $event = Net::Nostr::RemoteSigning->discovery_event(
    pubkey           => $remote_signer_pubkey,
    relays           => [$relay1, $relay2],      # optional
    nostrconnect_url => 'https://signer.example.com/<nostrconnect>',  # optional
);

Creates a NIP-89 kind 31990 discovery Net::Nostr::Event with a k tag of 24133. Optionally includes relay and nostrconnect_url tags. Croaks if pubkey is missing or not 64-char lowercase hex.

OBJECTS

BunkerConnection

Returned by "parse_bunker_uri". Croaks on unknown arguments.

remote_signer_pubkey - 32-byte hex public key of the remote signer
relays - Arrayref of relay URLs
secret - Optional secret for the connection

NostrConnect

Returned by "parse_nostrconnect_uri". Croaks on unknown arguments.

client_pubkey - 32-byte hex public key of the client
relays - Arrayref of relay URLs
secret - Secret string for connection validation
perms - Optional comma-separated permissions string
name - Optional client application name
url - Optional canonical URL of the client application
image - Optional image URL for the client application

Nip05Metadata

Returned by "parse_nip05_metadata". Croaks on unknown arguments.

pubkey - 32-byte hex public key from the names._ field
relays - Arrayref of relay URLs from the nip46.relays field
nostrconnect_url - Optional nostrconnect URL template

Discovery

Returned by "parse_discovery_event". Croaks on unknown arguments.

pubkey - 32-byte hex public key of the remote signer (event author)
relays - Arrayref of relay URLs from relay tags
nostrconnect_url - Optional nostrconnect URL from nostrconnect_url tag

Request

Returned by "parse_request". Croaks on unknown arguments or missing required fields (id, method, params). params must be an arrayref.

id - Request ID string
method - Method name (e.g. 'sign_event', 'ping')
params - Arrayref of string parameters

Response

Returned by "parse_response". Croaks on unknown arguments or missing id.

id - Request ID this response corresponds to
result - Result string, or undef on error
error - Error string, or undef on success
is_error - Returns true if this is an error response
is_auth_challenge - Returns true if this is an auth challenge (result is "auth_url")
auth_url - Returns the auth URL from the error field if this is an auth challenge

SEE ALSO

NIP-46, Net::Nostr, Net::Nostr::Event, Net::Nostr::Encryption