-module(acumen@challenge). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/acumen/challenge.gleam"). -export([dns01_txt_record/1, issuer_domain_names/1, status/1, token/1, key_authorization/2, url/1, dns_persist01_txt_record/2, challenge_type_of/1, find_by_type/2, dns_account_label/1, dns_account01_txt_record/3, error_decoder/0, decoder/0, optional_decoder/0]). -export_type([challenge/0, challenge_type/0, status/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Challenge types for ACME domain validation.\n" "\n" " After creating an order, complete challenges to prove control over the\n" " requested identifiers.\n" ). -type challenge() :: {http01_challenge, acumen@url:url(), status(), binary(), gleam@option:option(gleam@time@timestamp:timestamp()), gleam@option:option(acumen:acme_error())} | {dns01_challenge, acumen@url:url(), status(), binary(), gleam@option:option(gleam@time@timestamp:timestamp()), gleam@option:option(acumen:acme_error())} | {tls_alpn01_challenge, acumen@url:url(), status(), binary(), gleam@option:option(gleam@time@timestamp:timestamp()), gleam@option:option(acumen:acme_error())} | {dns_account01_challenge, acumen@url:url(), status(), binary(), gleam@option:option(gleam@time@timestamp:timestamp()), gleam@option:option(acumen:acme_error())} | {dns_persist01_challenge, acumen@url:url(), status(), gleam@option:option(gleam@time@timestamp:timestamp()), gleam@option:option(acumen:acme_error()), list(binary())}. -type challenge_type() :: http01 | dns01 | tls_alpn01 | dns_persist01 | dns_account01. -type status() :: pending | processing | valid | invalid. -file("src/acumen/challenge.gleam", 103). ?DOC( " Computes the DNS-01 TXT record value.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let assert Ok(key_auth) = challenge.key_authorization(dns_challenge, registered_key)\n" " let assert Ok(txt_value) = challenge.dns01_txt_record(key_auth)\n" " // Create TXT record: _acme-challenge.example.com -> txt_value\n" " ```\n" ). -spec dns01_txt_record(binary()) -> {ok, binary()} | {error, acumen:acme_error()}. dns01_txt_record(Key_authorization) -> _pipe = kryptos@crypto:hash( sha256, gleam_stdlib:identity(Key_authorization) ), _pipe@1 = gleam@result:map( _pipe, fun(Digest) -> gleam@bit_array:base64_url_encode(Digest, false) end ), gleam@result:replace_error( _pipe@1, {crypto_error, <<"SHA-256 hash failed"/utf8>>} ). -file("src/acumen/challenge.gleam", 175). ?DOC( " Returns the issuer domain names for a DNS-Persist-01 challenge.\n" "\n" " Returns `Error(Nil)` for other challenge types.\n" ). -spec issuer_domain_names(challenge()) -> {ok, list(binary())} | {error, nil}. issuer_domain_names(Challenge) -> case Challenge of {dns_persist01_challenge, _, _, _, _, Issuer_domain_names} -> {ok, Issuer_domain_names}; {http01_challenge, _, _, _, _, _} -> {error, nil}; {dns01_challenge, _, _, _, _, _} -> {error, nil}; {tls_alpn01_challenge, _, _, _, _, _} -> {error, nil}; {dns_account01_challenge, _, _, _, _, _} -> {error, nil} end. -file("src/acumen/challenge.gleam", 215). ?DOC(" Returns the status of a challenge.\n"). -spec status(challenge()) -> status(). status(Challenge) -> erlang:element(3, Challenge). -file("src/acumen/challenge.gleam", 222). ?DOC( " Returns the token for a challenge.\n" "\n" " Returns `Error(Nil)` for challenge types without a token (e.g. DNS-Persist-01).\n" ). -spec token(challenge()) -> {ok, binary()} | {error, nil}. token(Challenge) -> case Challenge of {http01_challenge, _, _, Token, _, _} -> {ok, Token}; {dns01_challenge, _, _, Token@1, _, _} -> {ok, Token@1}; {tls_alpn01_challenge, _, _, Token@2, _, _} -> {ok, Token@2}; {dns_account01_challenge, _, _, Token@3, _, _} -> {ok, Token@3}; {dns_persist01_challenge, _, _, _, _, _} -> {error, nil} end. -file("src/acumen/challenge.gleam", 196). ?DOC( " Computes the key authorization string for a challenge.\n" "\n" " Returns `Error(acumen.InvalidChallenge(_))` for challenge types that\n" " don't have a token (e.g., `DnsPersist01Challenge`).\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let assert Ok(key_auth) = challenge.key_authorization(http_challenge, registered_key)\n" " // Deploy: GET /.well-known/acme-challenge/{token} -> key_auth\n" " ```\n" ). -spec key_authorization(challenge(), acumen:registered_key()) -> {ok, binary()} | {error, acumen:acme_error()}. key_authorization(Challenge, Key) -> gleam@result:'try'( begin _pipe = token(Challenge), gleam@result:replace_error( _pipe, {invalid_challenge, <<"Challenge type does not have a token"/utf8>>} ) end, fun(Tkn) -> _pipe@1 = gose@jwk:thumbprint(erlang:element(2, Key), sha256), _pipe@2 = gleam@result:map_error( _pipe@1, fun(Err) -> {crypto_error, acumen@internal@utils:gose_error_to_string(Err)} end ), gleam@result:map( _pipe@2, fun(Thumbprint) -> <<<>/binary, Thumbprint/binary>> end ) end ). -file("src/acumen/challenge.gleam", 233). ?DOC(" Returns the URL for a challenge.\n"). -spec url(challenge()) -> acumen@url:url(). url(Challenge) -> erlang:element(2, Challenge). -file("src/acumen/challenge.gleam", 150). ?DOC( " Builds a DNS-Persist-01 TXT record value for a single issuer.\n" "\n" " A DNS-Persist-01 challenge may include multiple issuer domain names.\n" " Callers should create a separate TXT record for each issuer, all on the\n" " same `_validation-persist.{domain}` DNS name.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let assert Ok(issuers) = challenge.issuer_domain_names(dns_persist_challenge)\n" " let txt_records = list.map(issuers, fn(issuer) {\n" " challenge.dns_persist01_txt_record(issuer:, account_url: registered_key.kid)\n" " })\n" " // Create a TXT record at _validation-persist.example.com for each value\n" " ```\n" ). -spec dns_persist01_txt_record(binary(), acumen@url:url()) -> binary(). dns_persist01_txt_record(Issuer_domain_name, Account_url) -> <<< challenge_type(). challenge_type_of(Challenge) -> case Challenge of {http01_challenge, _, _, _, _, _} -> http01; {dns01_challenge, _, _, _, _, _} -> dns01; {tls_alpn01_challenge, _, _, _, _, _} -> tls_alpn01; {dns_account01_challenge, _, _, _, _, _} -> dns_account01; {dns_persist01_challenge, _, _, _, _, _} -> dns_persist01 end. -file("src/acumen/challenge.gleam", 165). ?DOC( " Finds the first challenge matching the given type, or `Error(Nil)` if none.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let assert Ok(http_challenge) =\n" " challenge.find_by_type(authorization.challenges, of: challenge.Http01)\n" " ```\n" ). -spec find_by_type(list(challenge()), challenge_type()) -> {ok, challenge()} | {error, nil}. find_by_type(Challenges, Type_) -> gleam@list:find(Challenges, fun(C) -> challenge_type_of(C) =:= Type_ end). -file("src/acumen/challenge.gleam", 341). ?DOC(false). -spec dns_account_label(binary()) -> {ok, binary()} | {error, acumen:acme_error()}. dns_account_label(Account_url) -> case kryptos@crypto:hash(sha256, gleam_stdlib:identity(Account_url)) of {error, _} -> {error, {crypto_error, <<"SHA-256 hash failed"/utf8>>}}; {ok, <>} -> Label = begin _pipe = Prefix, _pipe@1 = thirtytwo:encode(_pipe, false), string:lowercase(_pipe@1) end, {ok, Label}; {ok, _} -> {error, {crypto_error, <<"unexpected hash digest length"/utf8>>}} end. -file("src/acumen/challenge.gleam", 124). ?DOC( " Computes the DNS-Account-01 TXT record name and value.\n" "\n" " Returns `#(name, value)` where `name` is the full record name and\n" " `value` is the record content.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let assert Ok(key_auth) = challenge.key_authorization(dns_acct_challenge, registered_key)\n" " let assert Ok(#(record_name, record_value)) =\n" " challenge.dns_account01_txt_record(\"example.com\", registered_key.kid, key_auth)\n" " // Create TXT record: record_name -> record_value\n" " ```\n" ). -spec dns_account01_txt_record(binary(), acumen@url:url(), binary()) -> {ok, {binary(), binary()}} | {error, acumen:acme_error()}. dns_account01_txt_record(Domain, Account_url, Key_authorization) -> gleam@result:'try'( dns_account_label(acumen@url:to_string(Account_url)), fun(Label) -> gleam@result:'try'( dns01_txt_record(Key_authorization), fun(Value) -> Name = <<<<<<"_acme-challenge_"/utf8, Label/binary>>/binary, "."/utf8>>/binary, Domain/binary>>, {ok, {Name, Value}} end ) end ). -file("src/acumen/challenge.gleam", 358). ?DOC(false). -spec error_decoder() -> gleam@dynamic@decode:decoder(gleam@option:option(acumen:acme_error())). error_decoder() -> gleam@dynamic@decode:optional( begin gleam@dynamic@decode:field( <<"type"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Type_) -> gleam@dynamic@decode:optional_field( <<"detail"/utf8>>, <<""/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Detail) -> gleam@dynamic@decode:optional_field( <<"instance"/utf8>>, none, gleam@dynamic@decode:optional( {decoder, fun gleam@dynamic@decode:decode_string/1} ), fun(Instance) -> gleam@dynamic@decode:optional_field( <<"subproblems"/utf8>>, [], gleam@dynamic@decode:list( acumen:subproblem_decoder() ), fun(Subproblems) -> gleam@dynamic@decode:success( acumen:acme_error_from_type( Type_, Detail, Instance, Subproblems, none ) ) end ) end ) end ) end ) end ). -file("src/acumen/challenge.gleam", 382). -spec status_decoder() -> gleam@dynamic@decode:decoder(status()). status_decoder() -> gleam@dynamic@decode:then( {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(String) -> case String of <<"pending"/utf8>> -> gleam@dynamic@decode:success(pending); <<"processing"/utf8>> -> gleam@dynamic@decode:success(processing); <<"valid"/utf8>> -> gleam@dynamic@decode:success(valid); <<"invalid"/utf8>> -> gleam@dynamic@decode:success(invalid); _ -> gleam@dynamic@decode:failure( pending, <<"ChallengeStatus"/utf8>> ) end end ). -file("src/acumen/challenge.gleam", 292). -spec dns_persist_challenge_decoder() -> gleam@dynamic@decode:decoder(challenge()). dns_persist_challenge_decoder() -> gleam@dynamic@decode:field( <<"url"/utf8>>, acumen@url:decoder(), fun(Url) -> gleam@dynamic@decode:field( <<"status"/utf8>>, status_decoder(), fun(Status) -> gleam@dynamic@decode:optional_field( <<"validated"/utf8>>, none, gleam@dynamic@decode:optional( acumen@internal@utils:timestamp_decoder() ), fun(Validated) -> gleam@dynamic@decode:optional_field( <<"error"/utf8>>, none, error_decoder(), fun(Error) -> gleam@dynamic@decode:field( <<"issuer-domain-names"/utf8>>, gleam@dynamic@decode:list( {decoder, fun gleam@dynamic@decode:decode_string/1} ), fun(Issuer_domain_names) -> gleam@dynamic@decode:success( {dns_persist01_challenge, Url, Status, Validated, Error, Issuer_domain_names} ) end ) end ) end ) end ) end ). -file("src/acumen/challenge.gleam", 314). -spec standard_challenge_decoder( fun((acumen@url:url(), status(), binary(), gleam@option:option(gleam@time@timestamp:timestamp()), gleam@option:option(acumen:acme_error())) -> challenge()) ) -> gleam@dynamic@decode:decoder(challenge()). standard_challenge_decoder(Constructor) -> gleam@dynamic@decode:field( <<"url"/utf8>>, acumen@url:decoder(), fun(Url) -> gleam@dynamic@decode:field( <<"status"/utf8>>, status_decoder(), fun(Status) -> gleam@dynamic@decode:field( <<"token"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Token) -> gleam@dynamic@decode:optional_field( <<"validated"/utf8>>, none, gleam@dynamic@decode:optional( acumen@internal@utils:timestamp_decoder() ), fun(Validated) -> gleam@dynamic@decode:optional_field( <<"error"/utf8>>, none, error_decoder(), fun(Error) -> gleam@dynamic@decode:success( Constructor( Url, Status, Token, Validated, Error ) ) end ) end ) end ) end ) end ). -file("src/acumen/challenge.gleam", 249). ?DOC(false). -spec decoder() -> gleam@dynamic@decode:decoder(challenge()). decoder() -> gleam@dynamic@decode:field( <<"type"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Type_) -> case Type_ of <<"http-01"/utf8>> -> standard_challenge_decoder( fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {http01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end ); <<"dns-01"/utf8>> -> standard_challenge_decoder( fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {dns01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end ); <<"tls-alpn-01"/utf8>> -> standard_challenge_decoder( fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {tls_alpn01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end ); <<"dns-persist-01"/utf8>> -> dns_persist_challenge_decoder(); <<"dns-account-01"/utf8>> -> standard_challenge_decoder( fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {dns_account01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end ); _ -> Placeholder@1 = case acumen@url:from_string( <<"https://placeholder"/utf8>> ) of {ok, Placeholder} -> Placeholder; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"acumen/challenge"/utf8>>, function => <<"decoder"/utf8>>, line => 258, value => _assert_fail, start => 7814, 'end' => 7881, pattern_start => 7825, pattern_end => 7840}) end, gleam@dynamic@decode:failure( {http01_challenge, Placeholder@1, pending, <<""/utf8>>, none, none}, <<"Challenge"/utf8>> ) end end ). -file("src/acumen/challenge.gleam", 274). ?DOC(false). -spec optional_decoder() -> gleam@dynamic@decode:decoder(gleam@option:option(challenge())). optional_decoder() -> gleam@dynamic@decode:field( <<"type"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Type_) -> case Type_ of <<"http-01"/utf8>> -> _pipe = standard_challenge_decoder( fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {http01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end ), gleam@dynamic@decode:map( _pipe, fun(Field@0) -> {some, Field@0} end ); <<"dns-01"/utf8>> -> _pipe@1 = standard_challenge_decoder( fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {dns01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end ), gleam@dynamic@decode:map( _pipe@1, fun(Field@0) -> {some, Field@0} end ); <<"tls-alpn-01"/utf8>> -> _pipe@2 = standard_challenge_decoder( fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {tls_alpn01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end ), gleam@dynamic@decode:map( _pipe@2, fun(Field@0) -> {some, Field@0} end ); <<"dns-persist-01"/utf8>> -> _pipe@3 = dns_persist_challenge_decoder(), gleam@dynamic@decode:map( _pipe@3, fun(Field@0) -> {some, Field@0} end ); <<"dns-account-01"/utf8>> -> _pipe@4 = standard_challenge_decoder( fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {dns_account01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end ), gleam@dynamic@decode:map( _pipe@4, fun(Field@0) -> {some, Field@0} end ); _ -> gleam@dynamic@decode:success(none) end end ).