At

 view release on metacpan or  search on metacpan

lib/At/Protocol/DID.pm  view on Meta::CPAN


    #~ Taken from https://github.com/bluesky-social/atproto/blob/main/packages/syntax/src/did.ts
    #~ Human-readable constraints:
    #~   - valid W3C DID (https://www.w3.org/TR/did-core/#did-syntax)
    #~      - entire URI is ASCII: [a-zA-Z0-9._:%-]
    #~      - always starts "did:" (lower-case)
    #~      - method name is one or more lower-case letters, followed by ":"
    #~      - remaining identifier can have any of the above chars, but can not end in ":"
    #~      - it seems that a bunch of ":" can be included, and don't need spaces between
    #~      - "%" is used only for "percent encoding" and must be followed by two hex characters (and thus can't end in "%")
    #~      - query ("?") and fragment ("#") stuff is defined for "DID URIs", but not as part of identifier itself
    #~      - "The current specification does not take a position on the maximum length of a DID"
    #~   - in current atproto, only allowing did:plc and did:web. But not *forcing* this at lexicon layer
    #~   - hard length limit of 8KBytes
    #~   - not going to validate "percent encoding" here
    sub ensureValidDid ($did) {

        # check that all chars are boring ASCII
        throw InvalidDidError('Disallowed characters in DID (ASCII letters, digits, and a couple other characters only)')
            unless $did =~ /^[a-zA-Z0-9._:%-]*$/;
        #

lib/At/Protocol/URI.pm  view on Meta::CPAN

    #~    - follows ATURI docs on website
    #~       - all ASCII characters, no whitespace. non-ASCII could be URL-encoded
    #~       - starts "at://"
    #~       - "authority" is a valid DID or a valid handle
    #~       - optionally, follow "authority" with "/" and valid NSID as start of path
    #~       - optionally, if NSID given, follow that with "/" and rkey
    #~       - rkey path component can include URL-encoded ("percent encoded"), or:
    #~           ALPHA / DIGIT / "-" / "." / "_" / "~" / ":" / "@" / "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
    #~           [a-zA-Z0-9._~:@!$&'\(\)*+,;=-]
    #~       - rkey must have at least one char
    #~       - regardless of path component, a fragment can follow  as "#" and then a JSON pointer (RFC-6901)
    sub ensureValidAtUri($uri) {
        my $fragmentPart;
        my @uriParts = split '#', $uri, -1;    # negative limit, ftw
        throw InvalidAtUriError('ATURI can have at most one "#", separating fragment out') if scalar @uriParts > 2;
        $fragmentPart = $uriParts[1];
        $uri          = $uriParts[0];

        # Check that all chars are boring ASCII
        throw InvalidAtUriError('Disallowed characters in ATURI (ASCII)') unless $uri =~ /^[a-zA-Z0-9._~:@!\$&')(*+,;=%\/-]*$/;
        #
        my @parts = split /\//, $uri, -1;      # negative limit, ftw
        throw InvalidAtUriError('ATURI must start with "at://"') if scalar @parts >= 3 && ( $parts[0] ne 'at:' || length $parts[1] );
        throw InvalidAtUriError('ATURI requires at least method and authority sections') if scalar @parts < 3;
        try {
            if   ( $parts[2] =~ m/^did:/ ) { ensureValidDid( $parts[2] ); }

lib/At/Protocol/URI.pm  view on Meta::CPAN

            catch ($err) {
                throw InvalidAtUriError('ATURI requires first path segment (if supplied) to be valid NSID')
            }
        }
        if ( scalar @parts >= 5 ) {
            throw InvalidAtUriError('ATURI can not have a slash after collection, unless record key is provided') if !length $parts[4]

            # would validate rkey here, but there are basically no constraints!
        }
        throw InvalidAtUriError('ATURI path can have at most two parts, and no trailing slash') if scalar @parts >= 6;
        throw InvalidAtUriError('ATURI fragment must be non-empty and start with slash')        if scalar @uriParts >= 2 && !defined $fragmentPart;
        if ( defined $fragmentPart ) {
            throw InvalidAtUriError('ATURI fragment must be non-empty and start with slash')
                if length $fragmentPart == 0 || substr( $fragmentPart, 0, 1 ) ne '/';

            # NOTE: enforcing *some* checks here for sanity. Eg, at least no whitespace
            throw InvalidAtUriError( 'Disallowed characters in ATURI fragment (ASCII)' . $fragmentPart )
                if $fragmentPart !~ /^\/[a-zA-Z0-9._~:@!\$&')(*+,;=%[\]\/-]*$/;
        }
        throw InvalidAtUriError('ATURI is far too long') if length $uri > 8 * 1024;
        1;
    }

    sub ensureValidAtUriRegex($uri) {

        #~ simple regex to enforce most constraints via just regex and length.
        my $aturiRegex
            = qr/^at:\/\/(?<authority>[a-zA-Z0-9._:%-]+)(\/(?<collection>[a-zA-Z0-9-.]+)(\/(?<rkey>[a-zA-Z0-9._~:@!\$&%')(*+,;=-]+))?)?(#(?<fragment>\/[a-zA-Z0-9._~:@!\$&%')(*+,;=\-[\]\/\\]*))?$/;
        my ($rm) = $uri =~ $aturiRegex;
        throw InvalidAtUriError(q[ATURI didn't validate via regex]) if !$rm || !keys %+;
        my %groups = %+;
        try {
            ensureValidHandleRegex( $groups{authority} )
        }
        catch ($err) {
            try {
                ensureValidDidRegex( $groups{authority} )
            }

lib/At/UserAgent.pm  view on Meta::CPAN

        }
    }

    method _generate_dpop_proof( $url, $method, $skip_ath = 0 ) {
        return unless $dpop_key;
        my $jwk_json = $dpop_key->export_key_jwk('public');
        my $jwk      = JSON::PP::decode_json($jwk_json);
        my $now      = time;
        my $htu      = URI->new($url);
        $htu->query(undef);
        $htu->fragment(undef);
        my $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~';
        my $payload
            = { jti => Crypt::PRNG::random_string_from( $chars, 32 ), htm => $method, htu => $htu->as_string, iat => $now, exp => $now + 60, };
        $payload->{nonce} = $dpop_nonce if defined $dpop_nonce;

        if ( $accessJwt && !$skip_ath ) {
            $payload->{ath} = MIME::Base64::encode_base64url( Digest::SHA::sha256($accessJwt) );
            $payload->{ath} =~ s/=+$//;
        }
        return Crypt::JWT::encode_jwt( payload => $payload, key => $dpop_key, alg => 'ES256', extra_headers => { typ => 'dpop+jwt', jwk => $jwk } );

share/lexicons/com/germnetwork/declaration.json  view on Meta::CPAN

          }
        }
      }
    },
    "messageMe": {
      "type": "object",
      "required": ["showButtonTo", "messageMeUrl"],
      "properties": {
        "messageMeUrl": {
          "type": "string",
          "description": "A URL to present to an account that does not have its own com.germnetwork.declaration record, must have an empty fragment component, where the app should fill in the fragment component with the DIDs of the two accounts who w...
          "format": "uri",
          "minLength": 1,
          "maxLength": 2047
        },
        "showButtonTo": {
          "type": "string",
          "knownValues": ["none", "usersIFollow", "everyone"],
          "description": "The policy of who can message the account, this value is included in the keyPackage, but is duplicated here to allow applications to decide if they should show a 'Message on Germ' button to the viewer.",
          "minLength": 1,
          "maxLength": 100

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

            ok dies { ensureValidAtUriRegex($uri) }, 'ensureValidAtUriRegex( ... ) dies';
        }
    }
    #
    subtest 'enfore spec basics' => sub {
        expectValid('at://did:plc:asdf123');
        expectValid('at://user.bsky.social');
        expectValid('at://did:plc:asdf123/com.atproto.feed.post');
        expectValid('at://did:plc:asdf123/com.atproto.feed.post/record');
        #
        expectValid('at://did:plc:asdf123#/frag');
        expectValid('at://user.bsky.social#/frag');
        expectValid('at://did:plc:asdf123/com.atproto.feed.post#/frag');
        expectValid('at://did:plc:asdf123/com.atproto.feed.post/record#/frag');
        #
        expectInvalid('a://did:plc:asdf123');
        expectInvalid('at//did:plc:asdf123');
        expectInvalid('at:/a/did:plc:asdf123');
        expectInvalid('at:/did:plc:asdf123');
        expectInvalid('AT://did:plc:asdf123');
        expectInvalid('http://did:plc:asdf123');
        expectInvalid('://did:plc:asdf123');
        expectInvalid('at:did:plc:asdf123');
        expectInvalid('at:/did:plc:asdf123');
        expectInvalid('at:///did:plc:asdf123');
        expectInvalid('at://:/did:plc:asdf123');
        expectInvalid('at:/ /did:plc:asdf123');
        expectInvalid('at://did:plc:asdf123 ');
        expectInvalid('at://did:plc:asdf123/ ');
        expectInvalid(' at://did:plc:asdf123');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.post ');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.post# ');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.post#/ ');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.post#/frag ');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.post#fr ag');
        expectInvalid('//did:plc:asdf123');
        expectInvalid('at://name');
        expectInvalid('at://name.0');
        expectInvalid('at://diD:plc:asdf123');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.p@st');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.p$st');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.p%st');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.p&st');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.p()t');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed_post');
        expectInvalid('at://did:plc:asdf123/-com.atproto.feed.post');
        expectInvalid('at://did:plc:asdf@123/com.atproto.feed.post');
        #
        expectInvalid('at://DID:plc:asdf123');
        expectInvalid('at://user.bsky.123');
        expectInvalid('at://bsky');
        expectInvalid('at://did:plc:');
        expectInvalid('at://did:plc:');
        expectInvalid('at://frag');
        #
        expectValid( 'at://did:plc:asdf123/com.atproto.feed.post/' . ( 'o' x 800 ) );
        expectInvalid( 'at://did:plc:asdf123/com.atproto.feed.post/' . ( 'o' x 8200 ) );
    };
    subtest 'has specified behavior on edge cases' => sub {
        expectInvalid('at://user.bsky.social//');
        expectInvalid('at://user.bsky.social//com.atproto.feed.post');
        expectInvalid('at://user.bsky.social/com.atproto.feed.post//');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.post/asdf123/more/more');
        expectInvalid('at://did:plc:asdf123/short/stuff');

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

        expectInvalid('at://did:plc:asdf123/');
        #
        expectValid('at://user.bsky.social');
        expectInvalid('at://user.bsky.social/');
        #
        expectValid('at://did:plc:asdf123/com.atproto.feed.post');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.post/');
        #
        expectValid('at://did:plc:asdf123/com.atproto.feed.post/record');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.post/record/');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.post/record/#/frag');
    };
    subtest 'enforces strict paths' => sub {
        expectValid('at://did:plc:asdf123/com.atproto.feed.post/asdf123');
        expectInvalid('at://did:plc:asdf123/com.atproto.feed.post/asdf123/asdf');
    };
    subtest 'is very permissive about record keys' => sub {
        expectValid('at://did:plc:asdf123/com.atproto.feed.post/asdf123');
        expectValid('at://did:plc:asdf123/com.atproto.feed.post/a');
        expectValid('at://did:plc:asdf123/com.atproto.feed.post/%23');
        #

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

        expectValid('at://did:plc:asdf123/com.atproto.feed.post/;');
        expectValid('at://did:plc:asdf123/com.atproto.feed.post/abc%30123');
    };
    subtest 'is probably too permissive about URL encoding' => sub {
        expectValid('at://did:plc:asdf123/com.atproto.feed.post/%30');
        expectValid('at://did:plc:asdf123/com.atproto.feed.post/%3');
        expectValid('at://did:plc:asdf123/com.atproto.feed.post/%');
        expectValid('at://did:plc:asdf123/com.atproto.feed.post/%zz');
        expectValid('at://did:plc:asdf123/com.atproto.feed.post/%%%');
    };
    subtest 'is very permissive about fragments' => sub {
        expectValid('at://did:plc:asdf123#/frac');
        #
        expectInvalid('at://did:plc:asdf123#');
        expectInvalid('at://did:plc:asdf123##');
        expectInvalid('#at://did:plc:asdf123');
        expectInvalid('at://did:plc:asdf123#/asdf#/asdf');
        #
        expectValid('at://did:plc:asdf123#/com.atproto.feed.post');
        expectValid('at://did:plc:asdf123#/com.atproto.feed.post/');
        expectValid('at://did:plc:asdf123#/asdf/');

t/interop-test-files/syntax/aturi_syntax_invalid.txt  view on Meta::CPAN

at:/did:plc:asdf123
at:///did:plc:asdf123
at://:/did:plc:asdf123
at:/ /did:plc:asdf123
at://did:plc:asdf123 
at://did:plc:asdf123/ 
 at://did:plc:asdf123
at://did:plc:asdf123/com.atproto.feed.post 
at://did:plc:asdf123/com.atproto.feed.post# 
at://did:plc:asdf123/com.atproto.feed.post#/ 
at://did:plc:asdf123/com.atproto.feed.post#/frag 
at://did:plc:asdf123/com.atproto.feed.post#fr ag
//did:plc:asdf123
at://name
at://name.0
at://diD:plc:asdf123
at://did:plc:asdf123/com.atproto.feed.p@st
at://did:plc:asdf123/com.atproto.feed.p$st
at://did:plc:asdf123/com.atproto.feed.p%st
at://did:plc:asdf123/com.atproto.feed.p&st
at://did:plc:asdf123/com.atproto.feed.p()t
at://did:plc:asdf123/com.atproto.feed_post
at://did:plc:asdf123/-com.atproto.feed.post
at://did:plc:asdf@123/com.atproto.feed.post
at://DID:plc:asdf123
at://user.bsky.123
at://bsky
at://did:plc:
at://did:plc:
at://frag

# too long: 'at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(8200)
at://did:plc:asdf123/com.atproto.feed.post/ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...

# has specified behavior on edge cases
at://user.bsky.social//
at://user.bsky.social//com.atproto.feed.post
at://user.bsky.social/com.atproto.feed.post//
at://did:plc:asdf123/com.atproto.feed.post/asdf123/more/more',
at://did:plc:asdf123/short/stuff
at://did:plc:asdf123/12345

# enforces no trailing slashes
at://did:plc:asdf123/
at://user.bsky.social/
at://did:plc:asdf123/com.atproto.feed.post/
at://did:plc:asdf123/com.atproto.feed.post/record/
at://did:plc:asdf123/com.atproto.feed.post/record/#/frag

# enforces strict paths
at://did:plc:asdf123/com.atproto.feed.post/asdf123/asdf

# is very permissive about fragments
at://did:plc:asdf123#
at://did:plc:asdf123##
#at://did:plc:asdf123
at://did:plc:asdf123#/asdf#/asdf

# new less permissive about record keys for Lexicon use (with recordkey more specified)
at://did:plc:asdf123/com.atproto.feed.post/%23
at://did:plc:asdf123/com.atproto.feed.post/$@!*)(:,;~.sdf123
at://did:plc:asdf123/com.atproto.feed.post/~'sdf123")
at://did:plc:asdf123/com.atproto.feed.post/$



( run in 0.885 second using v1.01-cache-2.11-cpan-364913b4093 )