Apertur-SDK
view release on metacpan or search on metacpan
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},
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 )