NAME

Net::Nostr::Blossom - strict Nostr/Blossom server-list integration helpers

SYNOPSIS

use Net::Nostr::Blossom;

my $bl = Net::Nostr::Blossom->new(
    servers => [
        'https://blossom.self.hosted',
        'https://cdn.blossom.cloud',
    ],
);

say $bl->primary_server;  # https://blossom.self.hosted

my $event = $bl->to_event(pubkey => $key->pubkey_hex);
$key->sign_event($event);
$client->publish($event);

my $parsed = Net::Nostr::Blossom->from_event($event);
for my $url ($parsed->servers) {
    say $url;
}

my $hash = 'a' x 64;
my ($found_hash, $ext) = Net::Nostr::Blossom->extract_hash(
    "https://old-server.com/$hash.png"
);

my @fallback = $bl->fallback_urls("https://dead-server.com/$hash.png");

use Digest::SHA qw(sha256_hex);
my $data = 'file contents';
my $expected_hash = sha256_hex($data);
Net::Nostr::Blossom->verify_sha256($data, $expected_hash);  # true

DESCRIPTION

Net::Nostr::Blossom implements the Nostr-facing pieces of NIP-B7 and BUD-03: kind:10063 Blossom server-list events, ordered server tags, hash extraction from Blossom-like URLs, fallback URL generation, and SHA-256 content verification.

This module does not implement Blossom HTTP client behavior, server behavior, uploads, authorization, payments, blob discovery, or other BUD APIs.

Server lists are ordered. The first server is the primary server. Duplicate servers are preserved because BUD-03 gives ordering semantic value and does not specify deduplication; this helper serializes the list exactly as provided after validation.

CONSTRUCTORS

new

Accepts named arguments as either a flat list or a single hash reference.

my $bl = Net::Nostr::Blossom->new;

my $bl = Net::Nostr::Blossom->new(
    servers => [
        'https://blossom.self.hosted',
        'https://cdn.blossom.cloud',
    ],
);

Creates a Blossom server list. servers, when provided, must be an arrayref. Each server URL is strictly validated. The constructor copies the list, so later caller mutations do not change the object.

Server URLs must be HTTP or HTTPS base URLs. They must include an authority and host, must not include userinfo, query, fragment, control characters, or space characters, and any explicit port must be between 1 and 65535. Paths are allowed. Bracketed IPv6 authorities are supported where URI supports them.

from_event

my $bl = Net::Nostr::Blossom->from_event($event);

Parses a kind:10063 Net::Nostr::Event into a server list. This parser accepts only Net::Nostr::Event objects, not plain hashrefs. Hashrefs should be parsed by Net::Nostr::Event first so event validation has a single owner.

from_event rejects missing events, non-event values, wrong or malformed kinds, malformed tag lists, server tags without URL values, invalid server URLs, and events with no server tags. Non-server tags are ignored.

my $event = Net::Nostr::Event->new(
    pubkey  => 'a' x 64,
    kind    => 10063,
    content => '',
    tags    => [['server', 'https://blossom.example.com']],
);
my $bl = Net::Nostr::Blossom->from_event($event);
say $bl->primary_server;

METHODS

servers

my @urls = $bl->servers;
my $urls = $bl->servers;

Returns the server URLs in order. In list context it returns a list. In scalar context it returns a new arrayref. Mutating the returned arrayref does not mutate the object.

primary_server

my $url = $bl->primary_server;

Returns the first server URL, or undef for an empty list.

server_tags

my $tags = $bl->server_tags;

Returns a new arrayref of ordered server tag arrays suitable for a kind:10063 event.

# [
#   ['server', 'https://blossom.self.hosted'],
#   ['server', 'https://cdn.blossom.cloud'],
# ]

to_event

my $event = $bl->to_event(pubkey => $pubkey_hex);
my $event = $bl->to_event(pubkey => $pubkey_hex, created_at => time());

Creates a kind:10063 Net::Nostr::Event with empty content and ordered server tags. Extra arguments are passed through to Net::Nostr::Event->new, but kind, content, and tags are always set from the server list. Croaks if the list is empty because BUD-03 requires at least one server tag.

extract_hash

my ($hash, $ext) = Net::Nostr::Blossom->extract_hash($url);

Extracts the last bounded 64-character hexadecimal string from the input and returns it lowercased with an optional alphanumeric extension. The extension is captured only when it appears immediately after the hash as .ext and is followed by the end of the input, a query, or a fragment. Longer hex runs are not matched.

Returns (undef, undef) when no hash exists.

my $hash = 'a' x 64;
my ($h, $ext) = Net::Nostr::Blossom->extract_hash(
    "https://server.com/$hash.pdf?download=1"
);
# $h   = 'aaaa...'  (64 lowercase hex chars)
# $ext = 'pdf'

fallback_urls

my @urls = $bl->fallback_urls($original_url);

Extracts the hash and optional extension from $original_url and generates fallback URLs using this object's ordered servers:

<server without trailing slash>/<hash>[.<ext>]

Returns an empty list when $original_url does not contain a bounded 64-character hex hash.

my $bl = Net::Nostr::Blossom->new(
    servers => ['https://blossom.self.hosted', 'https://cdn.blossom.cloud'],
);
my @urls = $bl->fallback_urls(
    "https://unavailable.com/${\ ('b' x 64)}.jpg"
);

verify_sha256

my $ok = Net::Nostr::Blossom->verify_sha256($data, $expected_hash);

Computes the SHA-256 hash of $data and compares it to $expected_hash. The expected hash must be a 64-character hexadecimal string; malformed expected hashes croak. Valid but non-matching hashes return false.

my $data = 'file contents';
my $hash = Digest::SHA::sha256_hex($data);
Net::Nostr::Blossom->verify_sha256($data, $hash);      # true
Net::Nostr::Blossom->verify_sha256('tampered', $hash); # false

SEE ALSO

NIP-B7, BUD-03, Net::Nostr::Event