Current section
Files
Jump to
Current section
Files
src/acumen@register_account.erl
-module(acumen@register_account).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/acumen/register_account.gleam").
-export([agree_to_terms/1, build/3, contacts/2, external_account_binding/3, only_existing/1, request/0, response/2]).
-export_type([request_builder/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(
" Register a new ACME account.\n"
"\n"
" Before requesting certificates, you must register an account with the\n"
" ACME server.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import acumen\n"
" import acumen/account.{type Account}\n"
" import acumen/register_account\n"
" import gose/jwk\n"
" import kryptos/ec\n"
"\n"
" // Generate an account key (store this securely!)\n"
" let key = jwk.generate_ec(ec.P256)\n"
" let unregistered = acumen.UnregisteredKey(key)\n"
"\n"
" // Build the registration request\n"
" let reg = register_account.request()\n"
" |> register_account.contacts([\"mailto:admin@example.com\"])\n"
" |> register_account.agree_to_terms\n"
"\n"
" // Execute the request\n"
" let assert Ok(#(resp, ctx)) = acumen.execute(\n"
" ctx,\n"
" build: register_account.build(reg, _, unregistered),\n"
" send: httpc.send,\n"
" )\n"
"\n"
" // Parse the response to get the account and registered key\n"
" let assert Ok(#(account, registered_key)) =\n"
" register_account.response(resp, unregistered)\n"
" ```\n"
).
-opaque request_builder() :: {request_builder,
list(binary()),
boolean(),
boolean(),
gleam@option:option(acumen@account:external_account_binding())}.
-file("src/acumen/register_account.gleam", 65).
?DOC(
" Indicates agreement to the ACME server's terms of service.\n"
"\n"
" Most ACME servers (including Let's Encrypt) require you to agree to their\n"
" terms of service. The ToS URL is available in `directory.meta.terms_of_service`.\n"
).
-spec agree_to_terms(request_builder()) -> request_builder().
agree_to_terms(Builder) ->
{request_builder,
erlang:element(2, Builder),
true,
erlang:element(4, Builder),
erlang:element(5, Builder)}.
-file("src/acumen/register_account.gleam", 96).
-spec build_payload(request_builder(), gleam@option:option(gleam@json:json())) -> gleam@json:json().
build_payload(Builder, Eab_json) ->
Fields = [],
Fields@1 = case erlang:element(2, Builder) of
[] ->
Fields;
Contacts ->
[{<<"contact"/utf8>>,
gleam@json:array(Contacts, fun gleam@json:string/1)} |
Fields]
end,
Fields@2 = case erlang:element(3, Builder) of
true ->
[{<<"termsOfServiceAgreed"/utf8>>, gleam@json:bool(true)} |
Fields@1];
false ->
Fields@1
end,
Fields@3 = case erlang:element(4, Builder) of
true ->
[{<<"onlyReturnExisting"/utf8>>, gleam@json:bool(true)} | Fields@2];
false ->
Fields@2
end,
Fields@4 = case Eab_json of
{some, Eab} ->
[{<<"externalAccountBinding"/utf8>>, Eab} | Fields@3];
none ->
Fields@3
end,
gleam@json:object(Fields@4).
-file("src/acumen/register_account.gleam", 125).
-spec build_eab_jws(
acumen@account:external_account_binding(),
acumen@url:url(),
acumen:unregistered_key()
) -> {ok, gleam@option:option(gleam@json:json())} | {error, acumen:acme_error()}.
build_eab_jws(Eab, Url, Key) ->
gleam@result:'try'(
begin
_pipe = gose@jwk:public_key(erlang:element(2, Key)),
gleam@result:map_error(
_pipe,
fun(E) ->
{jws_error, acumen@internal@utils:gose_error_to_string(E)}
end
)
end,
fun(Public_key) ->
Jwk_json = gose@jwk:to_json(Public_key),
Payload = gleam@json:to_string(Jwk_json),
_pipe@1 = acumen@internal@jws:sign_eab(
erlang:element(3, Eab),
erlang:element(2, Eab),
Payload,
Url
),
_pipe@2 = gleam@result:map(
_pipe@1,
fun(Field@0) -> {some, Field@0} end
),
gleam@result:map_error(
_pipe@2,
fun(Field@0) -> {jws_error, Field@0} end
)
end
).
-file("src/acumen/register_account.gleam", 73).
?DOC(
" Builds the HTTP request for account registration.\n"
"\n"
" The full JWK is embedded in the protected header since the key isn't\n"
" registered yet.\n"
).
-spec build(request_builder(), acumen:context(), acumen:unregistered_key()) -> {ok,
gleam@http@request:request(binary())} |
{error, acumen:acme_error()}.
build(Builder, Context, Key) ->
Eab_result = case erlang:element(5, Builder) of
{some, Eab} ->
build_eab_jws(
Eab,
erlang:element(3, erlang:element(2, Context)),
Key
);
none ->
{ok, none}
end,
gleam@result:'try'(
Eab_result,
fun(Eab_json) -> _pipe = build_payload(Builder, Eab_json),
_pipe@1 = gleam@json:to_string(_pipe),
_pipe@2 = acumen@internal@jws:sign_with_jwk(
erlang:element(2, Key),
_pipe@1,
erlang:element(3, Context),
erlang:element(3, erlang:element(2, Context))
),
_pipe@3 = gleam@result:map_error(
_pipe@2,
fun(Field@0) -> {jws_error, Field@0} end
),
gleam@result:'try'(
_pipe@3,
fun(_capture) ->
acumen:build_post_request(
erlang:element(3, erlang:element(2, Context)),
_capture
)
end
) end
).
-file("src/acumen/register_account.gleam", 146).
?DOC(
" Adds contact URLs to the registration request.\n"
"\n"
" Contact URLs are typically `mailto:` addresses where the CA can reach you\n"
" about certificate expiration, policy changes, or security issues.\n"
).
-spec contacts(request_builder(), list(binary())) -> request_builder().
contacts(Builder, Contacts) ->
{request_builder,
Contacts,
erlang:element(3, Builder),
erlang:element(4, Builder),
erlang:element(5, Builder)}.
-file("src/acumen/register_account.gleam", 167).
?DOC(
" Adds external account binding credentials to the registration request.\n"
"\n"
" Some CAs (like Google, ZeroSSL) require you to bind your ACME\n"
" account to an existing account with them. They provide a key ID and a\n"
" MAC key (usually base64url-encoded).\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(mac_key) = bit_array.base64_url_decode(mac_key_b64)\n"
" let reg = register_account.request()\n"
" |> register_account.external_account_binding(key_id: \"key-id-from-ca\", mac_key: mac_key)\n"
" |> register_account.agree_to_terms\n"
" ```\n"
).
-spec external_account_binding(request_builder(), binary(), bitstring()) -> request_builder().
external_account_binding(Builder, Key_id, Mac_key) ->
{request_builder,
erlang:element(2, Builder),
erlang:element(3, Builder),
erlang:element(4, Builder),
{some, {external_account_binding, Key_id, Mac_key}}}.
-file("src/acumen/register_account.gleam", 189).
?DOC(
" Configures the request to only return an existing account.\n"
"\n"
" When set, the server will return the existing account for this key if one\n"
" exists, or return an `AccountDoesNotExist` error if not. No new account\n"
" will be created.\n"
"\n"
" This is useful for recovering an account URL when you have the private key\n"
" but lost the account metadata.\n"
).
-spec only_existing(request_builder()) -> request_builder().
only_existing(Builder) ->
{request_builder,
erlang:element(2, Builder),
erlang:element(3, Builder),
true,
erlang:element(5, Builder)}.
-file("src/acumen/register_account.gleam", 202).
?DOC(
" Creates a new account registration request builder.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let reg = register_account.request()\n"
" |> register_account.contacts([\"mailto:admin@example.com\"])\n"
" |> register_account.agree_to_terms\n"
" ```\n"
).
-spec request() -> request_builder().
request() ->
{request_builder, [], false, false, none}.
-file("src/acumen/register_account.gleam", 228).
-spec parse_account_response(
gleam@http@response:response(binary()),
acumen:unregistered_key()
) -> {ok, {acumen@account:account(), acumen:registered_key()}} |
{error, acumen:acme_error()}.
parse_account_response(Resp, Key) ->
gleam@result:'try'(
begin
_pipe = gleam@http@response:get_header(Resp, <<"location"/utf8>>),
gleam@result:replace_error(
_pipe,
{invalid_response, <<"missing Location header"/utf8>>}
)
end,
fun(Location) ->
gleam@result:'try'(
begin
_pipe@1 = acumen@url:from_string(Location),
gleam@result:replace_error(
_pipe@1,
{invalid_response, <<"invalid Location URI"/utf8>>}
)
end,
fun(Kid) ->
_pipe@2 = gleam@json:parse(
erlang:element(4, Resp),
acumen@account:decoder()
),
_pipe@3 = gleam@result:map_error(
_pipe@2,
fun(Error) ->
{json_parse_error,
acumen@internal@utils:json_parse_error_message(
<<"account"/utf8>>,
Error
)}
end
),
gleam@result:map(
_pipe@3,
fun(Account) ->
Registered_key = {registered_key,
erlang:element(2, Key),
Kid},
{Account, Registered_key}
end
)
end
)
end
).
-file("src/acumen/register_account.gleam", 215).
?DOC(
" Parses the account registration response.\n"
"\n"
" Returns the account and a `RegisteredKey` (the key upgraded with the\n"
" account URL). Use the `RegisteredKey` for all subsequent operations.\n"
).
-spec response(
gleam@http@response:response(binary()),
acumen:unregistered_key()
) -> {ok, {acumen@account:account(), acumen:registered_key()}} |
{error, acumen:acme_error()}.
response(Resp, Key) ->
case erlang:element(2, Resp) of
200 ->
parse_account_response(Resp, Key);
201 ->
parse_account_response(Resp, Key);
_ ->
{error,
{invalid_response,
acumen@internal@utils:unexpected_status_message(
erlang:element(2, Resp)
)}}
end.