Apertur-SDK

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN

my $session = $client->sessions->create(
    label     => 'Wedding reception',
    password  => 's3cr3t',
    maxImages => 200,
);

# Retrieve session details
my $details = $client->sessions->get($session->{uuid});

# Verify a password-protected session before uploading
my $result = $client->sessions->verify_password($session->{uuid}, 's3cr3t');

# Check per-destination delivery status. Returns:
#   { status => 'pending|active|completed|expired',
#     files => [...], lastChanged => '<ISO 8601>' }
my $status = $client->sessions->delivery_status($session->{uuid});

# Long-poll: server holds the response up to 5 min until something changes.
# Passing poll_from automatically widens the per-request timeout to 360 s.
$status = $client->sessions->delivery_status(
    $session->{uuid},

README.md  view on Meta::CPAN

        close $fh;
        print "Saved $image->{id}\n";
    },
    interval => 3,
    timeout  => 60,
);
```

## Receiving Webhooks

Apertur signs every webhook payload so you can verify it was not tampered with. Three verification methods are available. See [Webhooks documentation](https://docs.apertur.ca/webhooks).

```perl
use Apertur::SDK::Signature qw(
    verify_webhook_signature
    verify_event_signature
    verify_svix_signature
);

# Image delivery webhook
my $valid = verify_webhook_signature($body, $signature, $secret);

# Event webhook (HMAC method)
my $valid = verify_event_signature($body, $timestamp, $signature, $secret);

# Event webhook (Svix method)
my $valid = verify_svix_signature($body, $svix_id, $timestamp, $signature, $secret);
```

## Destinations

Destinations define where uploaded images are delivered. See [Destinations documentation](https://docs.apertur.ca/destinations).

```perl
use Apertur::SDK;

my $client     = Apertur::SDK->new(api_key => 'aptr_live_...');

lib/Apertur/SDK.pm  view on Meta::CPAN

            warn "API error: " . $err->message;
        }
        else {
            die $err;
        }
    }

=head1 WEBHOOK VERIFICATION

    use Apertur::SDK::Signature qw(
        verify_webhook_signature
        verify_event_signature
        verify_svix_signature
    );

    my $valid = verify_webhook_signature($body, $signature, $secret);

=head1 ENCRYPTION

    use Apertur::SDK::Crypto qw(encrypt_image);

    my $result = encrypt_image($image_bytes, $pem_key);

Encryption requires optional dependencies C<Crypt::OpenSSL::RSA> and
C<CryptX>. These are loaded at runtime only when encryption functions
are called.

lib/Apertur/SDK/Resource/Sessions.pm  view on Meta::CPAN

    my $qs = _build_query_string(%params);
    return $self->{http}->request('GET', "/api/v1/sessions/recent$qs");
}

sub qr {
    my ($self, $uuid, %options) = @_;
    my $qs = _build_query_string(%options);
    return $self->{http}->request_raw('GET', "/api/v1/upload-sessions/$uuid/qr$qs");
}

sub verify_password {
    my ($self, $uuid, $password) = @_;
    return $self->{http}->request(
        'POST', "/api/v1/upload/$uuid/verify-password",
        body => encode_json({ password => $password }),
    );
}

sub delivery_status {
    my ($self, $uuid, %opts) = @_;
    my $path = "/api/v1/upload-sessions/$uuid/delivery-status";
    my %req_opts;
    if (defined $opts{poll_from}) {
        $path .= '?pollFrom=' . uri_escape($opts{poll_from});

lib/Apertur/SDK/Resource/Sessions.pm  view on Meta::CPAN


=item B<recent(%params)>

Returns recently created sessions with optional C<limit>.

=item B<qr($uuid, %options)>

Returns the QR code image as raw bytes. Options: C<format>, C<size>,
C<style>, C<fg>, C<bg>, C<borderSize>, C<borderColor>.

=item B<verify_password($uuid, $password)>

Verifies a password for a protected session.

=item B<delivery_status($uuid, %opts)>

Returns the delivery status snapshot for a session as a hashref:

    {
        status      => 'pending' | 'active' | 'completed' | 'expired',
        files       => [ { record_id => ..., filename => ..., size_bytes => ...,

lib/Apertur/SDK/Signature.pm  view on Meta::CPAN

package Apertur::SDK::Signature;

use strict;
use warnings;

use Digest::SHA qw(hmac_sha256 hmac_sha256_hex);
use MIME::Base64 qw(encode_base64 decode_base64);

use Exporter 'import';
our @EXPORT_OK = qw(
    verify_webhook_signature
    verify_event_signature
    verify_svix_signature
);

sub verify_webhook_signature {
    my ($body, $signature, $secret) = @_;

    my $expected = hmac_sha256_hex($body, $secret);
    my $sig = $signature;
    $sig =~ s/^sha256=//;

    return _timing_safe_eq($expected, $sig);
}

sub verify_event_signature {
    my ($body, $timestamp, $signature, $secret) = @_;

    my $signature_base = "${timestamp}.${body}";
    my $expected = hmac_sha256_hex($signature_base, $secret);
    my $sig = $signature;
    $sig =~ s/^sha256=//;

    return _timing_safe_eq($expected, $sig);
}

sub verify_svix_signature {
    my ($body, $svix_id, $timestamp, $signature, $secret) = @_;

    my $signature_base = "${svix_id}.${timestamp}.${body}";
    my $secret_bytes = pack('H*', $secret);
    my $expected_bytes = hmac_sha256($signature_base, $secret_bytes);
    my $expected = encode_base64($expected_bytes, '');

    my $sig = $signature;
    $sig =~ s/^v1,//;

lib/Apertur/SDK/Signature.pm  view on Meta::CPAN


__END__

=head1 NAME

Apertur::SDK::Signature - Webhook signature verification

=head1 SYNOPSIS

    use Apertur::SDK::Signature qw(
        verify_webhook_signature
        verify_event_signature
        verify_svix_signature
    );

    # Image delivery webhook
    my $valid = verify_webhook_signature($body, $signature, $secret);

    # Event webhook (HMAC method)
    my $valid = verify_event_signature($body, $timestamp, $signature, $secret);

    # Event webhook (Svix method)
    my $valid = verify_svix_signature($body, $svix_id, $timestamp, $signature, $secret);

=head1 DESCRIPTION

Provides functions to verify webhook signatures sent by the Apertur API.
All comparisons use constant-time algorithms to prevent timing attacks.

=head1 FUNCTIONS

=over 4

=item B<verify_webhook_signature($body, $signature, $secret)>

Verifies an image delivery webhook. The signature is expected to be in
the format C<sha256=E<lt>hexE<gt>>.

=item B<verify_event_signature($body, $timestamp, $signature, $secret)>

Verifies an event webhook using the HMAC method. The signed payload is
C<${timestamp}.${body}>.

=item B<verify_svix_signature($body, $svix_id, $timestamp, $signature, $secret)>

Verifies an event webhook using the Svix method. The signed payload is
C<${svix_id}.${timestamp}.${body}> and the secret is hex-decoded before
use. The signature is expected in the format C<v1,E<lt>base64E<gt>>.

=back

=cut

t/01_client.t  view on Meta::CPAN

    isa_ok($caught, 'Apertur::SDK::Error::NotFound');
    is($caught->message, 'Gone', 'thrown error message');
};

# --- Signature verification ---

subtest 'Signature verification' => sub {
    plan tests => 6;

    use Apertur::SDK::Signature qw(
        verify_webhook_signature
        verify_event_signature
        verify_svix_signature
    );

    # Webhook signature
    my $secret = 'my_secret';
    my $body   = '{"event":"test"}';

    use Digest::SHA qw(hmac_sha256_hex);
    my $sig_hex = hmac_sha256_hex($body, $secret);
    ok(
        verify_webhook_signature($body, "sha256=$sig_hex", $secret),
        'valid webhook signature accepted',
    );
    ok(
        !verify_webhook_signature($body, 'sha256=bad', $secret),
        'invalid webhook signature rejected',
    );

    # Event signature
    my $timestamp = '1700000000';
    my $event_sig = hmac_sha256_hex("${timestamp}.${body}", $secret);
    ok(
        verify_event_signature($body, $timestamp, "sha256=$event_sig", $secret),
        'valid event signature accepted',
    );
    ok(
        !verify_event_signature($body, $timestamp, 'sha256=bad', $secret),
        'invalid event signature rejected',
    );

    # Svix signature
    my $svix_id     = 'msg_abc123';
    my $svix_secret = 'deadbeef';
    my $svix_base   = "${svix_id}.${timestamp}.${body}";

    use MIME::Base64 qw(encode_base64);
    my $svix_expected = encode_base64(
        Digest::SHA::hmac_sha256($svix_base, pack('H*', $svix_secret)),
        '',
    );
    ok(
        verify_svix_signature($body, $svix_id, $timestamp, "v1,$svix_expected", $svix_secret),
        'valid svix signature accepted',
    );
    ok(
        !verify_svix_signature($body, $svix_id, $timestamp, 'v1,badsig==', $svix_secret),
        'invalid svix signature rejected',
    );
};

# --- Error stringification ---

subtest 'Error stringification' => sub {
    plan tests => 1;

    my $err = Apertur::SDK::Error->new(



( run in 1.217 second using v1.01-cache-2.11-cpan-e1769b4cff6 )