Skip to main content

src/aws@credentials.erl

-module(aws@credentials).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/aws/credentials.gleam").
-export([fetch/1, chain/1, static_provider/1, from_environment_with/1, from_environment/0, from_profile_with/3, from_profile/1, from_assume_role_with/9, from_profile_assume_role_with/7, from_profile_assume_role/3, from_imds_with/3, from_imds/1, from_ecs_with/3, from_ecs_with_env/3, from_ecs/1, from_web_identity_with_env/3, from_web_identity/1, from_web_identity_with/7, from_sso_with/5, from_sso_with_endpoint/6, from_sso_with_env/4, from_sso/2, from_process_with_runner/2, from_process_with_command/1, from_process_with_env/4, from_process/1, from_aws_cli_with/2, from_aws_cli/1, from_assume_role/6, default_chain_with/6, default_chain/2]).
-export_type([credentials/0, provider_error/0, provider/0, sso_config/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(
    " Credentials and credential providers.\n"
    "\n"
    " A `Provider` is a thin record wrapping a fetch function; providers compose\n"
    " into a `chain` that returns the first success and reports every attempt\n"
    " when it exhausts. The actual provider implementations (static, env,\n"
    " profile, IMDS, ECS, STS web identity, SSO, process) live in\n"
    " `aws/internal/providers/*` and are surfaced through builder functions on\n"
    " this module.\n"
    "\n"
    " The same `Credentials` value flows into SigV4 signing; the signer ignores\n"
    " the expiry/source metadata that's relevant only to the chain.\n"
).

-type credentials() :: {credentials,
        binary(),
        binary(),
        gleam@option:option(binary()),
        gleam@option:option(integer()),
        binary()}.

-type provider_error() :: {not_configured, binary()} |
    {fetch_failed, binary()} |
    {chain_exhausted, list({binary(), provider_error()})}.

-type provider() :: {provider,
        binary(),
        fun(() -> {ok, credentials()} | {error, provider_error()})}.

-type sso_config() :: {sso_config,
        binary(),
        binary(),
        binary(),
        binary(),
        binary()}.

-file("src/aws/credentials.gleam", 74).
?DOC(" Run a provider and return whatever it produced.\n").
-spec fetch(provider()) -> {ok, credentials()} | {error, provider_error()}.
fetch(Provider) ->
    (erlang:element(3, Provider))().

-file("src/aws/credentials.gleam", 119).
?DOC(
    " A provider declining mid-chain: a not-configured provider is an expected,\n"
    " quiet skip (`debug`); a configured-but-failing provider is notable enough\n"
    " to warn about even on the recovered path (`warning`, default-on).\n"
).
-spec log_provider_miss(binary(), provider_error()) -> nil.
log_provider_miss(Name, Reason) ->
    case Reason of
        {not_configured, R} ->
            aws@internal@log:debug(
                fun() ->
                    <<<<<<"aws credentials: skipped "/utf8, Name/binary>>/binary,
                            " — "/utf8>>/binary,
                        R/binary>>
                end
            );

        {fetch_failed, R@1} ->
            aws@internal@log:warning(
                <<<<<<"aws credentials: "/utf8, Name/binary>>/binary,
                        " failed — "/utf8>>/binary,
                    R@1/binary>>
            );

        {chain_exhausted, _} ->
            aws@internal@log:debug(
                fun() ->
                    <<<<"aws credentials: "/utf8, Name/binary>>/binary,
                        " sub-chain exhausted"/utf8>>
                end
            )
    end.

-file("src/aws/credentials.gleam", 130).
-spec attempted_names(list({binary(), provider_error()})) -> binary().
attempted_names(Attempts) ->
    _pipe = Attempts,
    _pipe@1 = lists:reverse(_pipe),
    _pipe@2 = gleam@list:map(
        _pipe@1,
        fun(Attempt) -> erlang:element(1, Attempt) end
    ),
    gleam@string:join(_pipe@2, <<", "/utf8>>).

-file("src/aws/credentials.gleam", 87).
-spec try_each(list(provider()), list({binary(), provider_error()})) -> {ok,
        credentials()} |
    {error, provider_error()}.
try_each(Providers, Attempts) ->
    case Providers of
        [] ->
            aws@internal@log:error(
                <<<<"aws credentials: chain exhausted — no provider supplied credentials (tried: "/utf8,
                        (attempted_names(Attempts))/binary>>/binary,
                    ")"/utf8>>
            ),
            {error, {chain_exhausted, lists:reverse(Attempts)}};

        [P | Rest] ->
            case (erlang:element(3, P))() of
                {ok, Credentials} ->
                    aws@internal@log:debug(
                        fun() ->
                            <<"aws credentials: resolved via "/utf8,
                                (erlang:element(2, P))/binary>>
                        end
                    ),
                    {ok, Credentials};

                {error, Reason} ->
                    log_provider_miss(erlang:element(2, P), Reason),
                    try_each(Rest, [{erlang:element(2, P), Reason} | Attempts])
            end
    end.

-file("src/aws/credentials.gleam", 83).
?DOC(
    " Compose providers into a single provider that walks them in order and\n"
    " returns the first success. If every provider fails, the resulting error is\n"
    " `ChainExhausted` with one `(name, error)` entry per attempt in the order\n"
    " they were tried — useful for debugging \"why didn't my IMDS creds get\n"
    " picked up?\" without having to instrument each provider individually.\n"
).
-spec chain(list(provider())) -> provider().
chain(Providers) ->
    {provider, <<"Chain"/utf8>>, fun() -> try_each(Providers, []) end}.

-file("src/aws/credentials.gleam", 140).
?DOC(
    " A provider that always returns the same hardcoded credentials. The primary\n"
    " use is tests and scripts where you have keys in hand; in production the\n"
    " chain pulls from env/profile/IMDS instead.\n"
).
-spec static_provider(credentials()) -> provider().
static_provider(Credentials) ->
    Labelled = {credentials,
        erlang:element(2, Credentials),
        erlang:element(3, Credentials),
        erlang:element(4, Credentials),
        erlang:element(5, Credentials),
        <<"Static"/utf8>>},
    {provider, <<"Static"/utf8>>, fun() -> {ok, Labelled} end}.

-file("src/aws/credentials.gleam", 162).
-spec fetch_from_env(fun((binary()) -> {ok, binary()} | {error, nil})) -> {ok,
        credentials()} |
    {error, provider_error()}.
fetch_from_env(Lookup) ->
    gleam@result:'try'(
        begin
            _pipe = Lookup(<<"AWS_ACCESS_KEY_ID"/utf8>>),
            gleam@result:replace_error(
                _pipe,
                {not_configured, <<"AWS_ACCESS_KEY_ID not set"/utf8>>}
            )
        end,
        fun(Access_key_id) ->
            gleam@result:'try'(
                begin
                    _pipe@1 = Lookup(<<"AWS_SECRET_ACCESS_KEY"/utf8>>),
                    gleam@result:replace_error(
                        _pipe@1,
                        {not_configured,
                            <<"AWS_SECRET_ACCESS_KEY not set"/utf8>>}
                    )
                end,
                fun(Secret_access_key) ->
                    case {gleam@string:is_empty(Access_key_id),
                        gleam@string:is_empty(Secret_access_key)} of
                        {true, _} ->
                            {error,
                                {not_configured,
                                    <<"AWS_ACCESS_KEY_ID is set but empty"/utf8>>}};

                        {_, true} ->
                            {error,
                                {not_configured,
                                    <<"AWS_SECRET_ACCESS_KEY is set but empty"/utf8>>}};

                        {false, false} ->
                            Session_token = case Lookup(
                                <<"AWS_SESSION_TOKEN"/utf8>>
                            ) of
                                {ok, Token} ->
                                    case gleam@string:is_empty(Token) of
                                        true ->
                                            none;

                                        false ->
                                            {some, Token}
                                    end;

                                {error, _} ->
                                    none
                            end,
                            {ok,
                                {credentials,
                                    Access_key_id,
                                    Secret_access_key,
                                    Session_token,
                                    none,
                                    <<"Environment"/utf8>>}}
                    end
                end
            )
        end
    ).

-file("src/aws/credentials.gleam", 151).
?DOC(
    " Environment-variable provider. Reads `AWS_ACCESS_KEY_ID`,\n"
    " `AWS_SECRET_ACCESS_KEY`, and (optionally) `AWS_SESSION_TOKEN`.\n"
    "\n"
    " `lookup` is injected so tests can drive the provider with a fixed map\n"
    " instead of mutating real process env. Use `from_environment` for the\n"
    " default production wiring.\n"
).
-spec from_environment_with(fun((binary()) -> {ok, binary()} | {error, nil})) -> provider().
from_environment_with(Lookup) ->
    {provider, <<"Environment"/utf8>>, fun() -> fetch_from_env(Lookup) end}.

-file("src/aws/credentials.gleam", 158).
?DOC(" Environment-variable provider using real OS env. Production default.\n").
-spec from_environment() -> provider().
from_environment() ->
    from_environment_with(fun aws_ffi:get_env/1).

-file("src/aws/credentials.gleam", 395).
-spec build_credentials_from_lookup(
    binary(),
    fun((binary()) -> {ok, binary()} | {error, nil})
) -> {ok, credentials()} | {error, provider_error()}.
build_credentials_from_lookup(Profile_name, Lookup) ->
    gleam@result:'try'(
        begin
            _pipe = Lookup(<<"aws_access_key_id"/utf8>>),
            gleam@result:replace_error(
                _pipe,
                {not_configured,
                    <<<<"profile '"/utf8, Profile_name/binary>>/binary,
                        "' has no aws_access_key_id"/utf8>>}
            )
        end,
        fun(Access_key_id) ->
            gleam@result:'try'(
                begin
                    _pipe@1 = Lookup(<<"aws_secret_access_key"/utf8>>),
                    gleam@result:replace_error(
                        _pipe@1,
                        {fetch_failed,
                            <<<<"profile '"/utf8, Profile_name/binary>>/binary,
                                "' has aws_access_key_id but no aws_secret_access_key"/utf8>>}
                    )
                end,
                fun(Secret_access_key) ->
                    Session_token = case Lookup(<<"aws_session_token"/utf8>>) of
                        {ok, T} ->
                            {some, T};

                        {error, _} ->
                            none
                    end,
                    {ok,
                        {credentials,
                            Access_key_id,
                            Secret_access_key,
                            Session_token,
                            none,
                            <<<<"Profile("/utf8, Profile_name/binary>>/binary,
                                ")"/utf8>>}}
                end
            )
        end
    ).

-file("src/aws/credentials.gleam", 353).
?DOC(
    " Returns a `(key) -> Result(value, Nil)` closure that walks the credentials\n"
    " file first, then the config file. Empty values count as absent so a half-\n"
    " commented-out key doesn't accidentally take effect.\n"
).
-spec merged_lookup(
    {ok, gleam@dict:dict(binary(), gleam@dict:dict(binary(), binary()))} |
        {error, provider_error()},
    {ok, gleam@dict:dict(binary(), gleam@dict:dict(binary(), binary()))} |
        {error, provider_error()},
    binary(),
    binary()
) -> fun((binary()) -> {ok, binary()} | {error, nil}).
merged_lookup(Parsed_creds, Parsed_config, Creds_section, Config_section) ->
    From_creds = fun(Key) -> case Parsed_creds of
            {ok, P} ->
                case aws@internal@ini:get_property(P, Creds_section, Key) of
                    {ok, V} ->
                        case V of
                            <<""/utf8>> ->
                                {error, nil};

                            _ ->
                                {ok, V}
                        end;

                    {error, _} ->
                        {error, nil}
                end;

            {error, _} ->
                {error, nil}
        end end,
    From_config = fun(Key@1) -> case Parsed_config of
            {ok, P@1} ->
                case aws@internal@ini:get_property(P@1, Config_section, Key@1) of
                    {ok, V@1} ->
                        case V@1 of
                            <<""/utf8>> ->
                                {error, nil};

                            _ ->
                                {ok, V@1}
                        end;

                    {error, _} ->
                        {error, nil}
                end;

            {error, _} ->
                {error, nil}
        end end,
    fun(Key@2) -> case From_creds(Key@2) of
            {ok, V@2} ->
                {ok, V@2};

            {error, _} ->
                From_config(Key@2)
        end end.

-file("src/aws/credentials.gleam", 332).
-spec parse_profile_file(fun(() -> {ok, binary()} | {error, nil})) -> {ok,
        gleam@dict:dict(binary(), gleam@dict:dict(binary(), binary()))} |
    {error, provider_error()}.
parse_profile_file(Reader) ->
    gleam@result:'try'(
        begin
            _pipe = Reader(),
            gleam@result:replace_error(
                _pipe,
                {not_configured, <<"file not readable"/utf8>>}
            )
        end,
        fun(Text) -> _pipe@1 = aws@internal@ini:parse(Text),
            gleam@result:map_error(
                _pipe@1,
                fun(E) ->
                    {fetch_failed,
                        <<<<<<"shared profile parse error at line "/utf8,
                                    (erlang:integer_to_binary(
                                        erlang:element(2, E)
                                    ))/binary>>/binary,
                                ": "/utf8>>/binary,
                            (erlang:element(3, E))/binary>>}
                end
            ) end
    ).

-file("src/aws/credentials.gleam", 298).
-spec fetch_from_profile(
    binary(),
    fun(() -> {ok, binary()} | {error, nil}),
    fun(() -> {ok, binary()} | {error, nil})
) -> {ok, credentials()} | {error, provider_error()}.
fetch_from_profile(Profile_name, Credentials_reader, Config_reader) ->
    Parsed_creds = parse_profile_file(Credentials_reader),
    Parsed_config = parse_profile_file(Config_reader),
    case {Parsed_creds, Parsed_config} of
        {{error, {not_configured, _}}, {error, {not_configured, _}}} ->
            {error,
                {not_configured,
                    <<"no AWS shared credentials or config file readable"/utf8>>}};

        {{error, {fetch_failed, R}}, _} ->
            {error, {fetch_failed, R}};

        {_, {error, {fetch_failed, R@1}}} ->
            {error, {fetch_failed, R@1}};

        {_, _} ->
            Creds_section = Profile_name,
            Config_section = case Profile_name of
                <<"default"/utf8>> ->
                    <<"default"/utf8>>;

                Other ->
                    <<"profile "/utf8, Other/binary>>
            end,
            Lookup = merged_lookup(
                Parsed_creds,
                Parsed_config,
                Creds_section,
                Config_section
            ),
            build_credentials_from_lookup(Profile_name, Lookup)
    end.

-file("src/aws/credentials.gleam", 221).
?DOC(
    " AWS shared credentials provider. Reads `[profile_name]` from both\n"
    " `~/.aws/credentials` (section name: `[name]`) and `~/.aws/config`\n"
    " (section name: `[profile name]`, or `[default]` for the default profile).\n"
    " If a property is set in both files, the credentials file wins — that's\n"
    " the AWS CLI convention. Either file may be missing.\n"
    "\n"
    " Both readers are injected so tests can drive the provider with in-memory\n"
    " strings; `from_profile` plugs in real readers for the two canonical paths.\n"
    "\n"
    " Errors:\n"
    "   - both readers fail → NotConfigured (no AWS config on this host)\n"
    "   - either file parses badly → FetchFailed (file exists but corrupt)\n"
    "   - profile section absent from both files → NotConfigured\n"
    "   - aws_access_key_id missing → NotConfigured (treat as \"this profile\n"
    "     isn't a static-key profile; chain should keep going\")\n"
    "   - aws_access_key_id present without aws_secret_access_key → FetchFailed\n"
).
-spec from_profile_with(
    binary(),
    fun(() -> {ok, binary()} | {error, nil}),
    fun(() -> {ok, binary()} | {error, nil})
) -> provider().
from_profile_with(Profile_name, Credentials_reader, Config_reader) ->
    {provider,
        <<<<"Profile("/utf8, Profile_name/binary>>/binary, ")"/utf8>>,
        fun() ->
            fetch_from_profile(Profile_name, Credentials_reader, Config_reader)
        end}.

-file("src/aws/credentials.gleam", 1053).
-spec read_default_config_file() -> {ok, binary()} | {error, nil}.
read_default_config_file() ->
    gleam@result:'try'(
        aws_ffi:get_env(<<"HOME"/utf8>>),
        fun(Home) ->
            Path = <<Home/binary, "/.aws/config"/utf8>>,
            gleam@result:'try'(
                aws_ffi:read_file(Path),
                fun(Bits) -> _pipe = gleam@bit_array:to_string(Bits),
                    gleam@result:replace_error(_pipe, nil) end
            )
        end
    ).

-file("src/aws/credentials.gleam", 291).
-spec read_default_profile_file() -> {ok, binary()} | {error, nil}.
read_default_profile_file() ->
    gleam@result:'try'(
        aws_ffi:get_env(<<"HOME"/utf8>>),
        fun(Home) ->
            Path = <<Home/binary, "/.aws/credentials"/utf8>>,
            gleam@result:'try'(
                aws_ffi:read_file(Path),
                fun(Bits) -> _pipe = gleam@bit_array:to_string(Bits),
                    gleam@result:replace_error(_pipe, nil) end
            )
        end
    ).

-file("src/aws/credentials.gleam", 233).
?DOC(
    " Profile provider using the canonical default file paths\n"
    " (`~/.aws/credentials` + `~/.aws/config`).\n"
).
-spec from_profile(binary()) -> provider().
from_profile(Profile_name) ->
    from_profile_with(
        Profile_name,
        fun read_default_profile_file/0,
        fun read_default_config_file/0
    ).

-file("src/aws/credentials.gleam", 1275).
?DOC(
    " Fully-explicit form — used by tests and callers that need a regional\n"
    " STS endpoint or a non-default session duration.\n"
).
-spec from_assume_role_with(
    provider(),
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary(),
    binary(),
    binary(),
    gleam@option:option(binary()),
    binary(),
    integer(),
    fun(() -> binary())
) -> provider().
from_assume_role_with(
    Source,
    Send,
    Region,
    Role_arn,
    Role_session_name,
    External_id,
    Endpoint,
    Duration_seconds,
    Timestamp
) ->
    Label = <<<<"AssumeRole("/utf8, Role_arn/binary>>/binary, ")"/utf8>>,
    Options = {options,
        Endpoint,
        Region,
        Role_arn,
        Role_session_name,
        Duration_seconds,
        External_id},
    {provider,
        Label,
        fun() ->
            gleam@result:'try'(
                (erlang:element(3, Source))(),
                fun(Outer) ->
                    Signing = {signing_credentials,
                        erlang:element(2, Outer),
                        erlang:element(3, Outer),
                        erlang:element(4, Outer)},
                    case aws@internal@providers@sts:fetch(
                        Send,
                        Signing,
                        Options,
                        Timestamp
                    ) of
                        {ok, C} ->
                            {ok,
                                {credentials,
                                    erlang:element(2, C),
                                    erlang:element(3, C),
                                    {some, erlang:element(4, C)},
                                    {some, erlang:element(5, C)},
                                    Label}};

                        {error, {misconfigured, Reason}} ->
                            {error, {not_configured, Reason}};

                        {error, {failed, Reason@1}} ->
                            {error, {fetch_failed, Reason@1}}
                    end
                end
            )
        end}.

-file("src/aws/credentials.gleam", 442).
?DOC(
    " Profile-builder variant that honours `role_arn` / `source_profile`:\n"
    " when the requested profile carries `role_arn`, this resolves the\n"
    " bootstrap credentials from `source_profile` (which must hold static\n"
    " keys), wraps them as a Provider, and chains via `from_assume_role`\n"
    " to fetch the role's temporary credentials. When `role_arn` is\n"
    " absent it falls through to the static-keys path identical to\n"
    " `fetch_from_profile`.\n"
    "\n"
    " `send` is the HTTP transport STS signs against. `region` is what\n"
    " STS signs as — the global STS endpoint accepts any region, so\n"
    " `\"us-east-1\"` is a safe default. `timestamp` is the wall-clock\n"
    " source for the SigV4 signer (injected so tests can pin it).\n"
    "\n"
    " Only a single chain hop is honoured today: `source_profile` must\n"
    " itself carry static keys, not another `role_arn`. Multi-hop chains\n"
    " (A → assumes B → assumes C) are a follow-up.\n"
).
-spec fetch_from_profile_with_assume_role(
    binary(),
    fun(() -> {ok, binary()} | {error, nil}),
    fun(() -> {ok, binary()} | {error, nil}),
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary(),
    binary(),
    fun(() -> binary())
) -> {ok, credentials()} | {error, provider_error()}.
fetch_from_profile_with_assume_role(
    Profile_name,
    Credentials_reader,
    Config_reader,
    Send,
    Region,
    Endpoint,
    Timestamp
) ->
    Parsed_creds = parse_profile_file(Credentials_reader),
    Parsed_config = parse_profile_file(Config_reader),
    case {Parsed_creds, Parsed_config} of
        {{error, {not_configured, _}}, {error, {not_configured, _}}} ->
            {error,
                {not_configured,
                    <<"no AWS shared credentials or config file readable"/utf8>>}};

        {{error, {fetch_failed, R}}, _} ->
            {error, {fetch_failed, R}};

        {_, {error, {fetch_failed, R@1}}} ->
            {error, {fetch_failed, R@1}};

        {_, _} ->
            Lookup_for = fun(Name) ->
                Creds_section = Name,
                Config_section = case Name of
                    <<"default"/utf8>> ->
                        <<"default"/utf8>>;

                    Other ->
                        <<"profile "/utf8, Other/binary>>
                end,
                merged_lookup(
                    Parsed_creds,
                    Parsed_config,
                    Creds_section,
                    Config_section
                )
            end,
            Our_lookup = Lookup_for(Profile_name),
            case Our_lookup(<<"role_arn"/utf8>>) of
                {error, _} ->
                    build_credentials_from_lookup(Profile_name, Our_lookup);

                {ok, Role_arn} ->
                    gleam@result:'try'(
                        begin
                            _pipe = Our_lookup(<<"source_profile"/utf8>>),
                            gleam@result:replace_error(
                                _pipe,
                                {not_configured,
                                    <<<<"profile '"/utf8, Profile_name/binary>>/binary,
                                        "' has role_arn but no source_profile"/utf8>>}
                            )
                        end,
                        fun(Source_name) ->
                            Source_lookup = Lookup_for(Source_name),
                            gleam@result:'try'(
                                build_credentials_from_lookup(
                                    Source_name,
                                    Source_lookup
                                ),
                                fun(Source_creds) ->
                                    Source_provider = {provider,
                                        <<<<"ProfileSource("/utf8,
                                                Source_name/binary>>/binary,
                                            ")"/utf8>>,
                                        fun() -> {ok, Source_creds} end},
                                    Session_name = case Our_lookup(
                                        <<"role_session_name"/utf8>>
                                    ) of
                                        {ok, N} ->
                                            N;

                                        {error, _} ->
                                            <<"aws-gleam-session"/utf8>>
                                    end,
                                    External_id = case Our_lookup(
                                        <<"external_id"/utf8>>
                                    ) of
                                        {ok, Eid} ->
                                            {some, Eid};

                                        {error, _} ->
                                            none
                                    end,
                                    Provider = from_assume_role_with(
                                        Source_provider,
                                        Send,
                                        Region,
                                        Role_arn,
                                        Session_name,
                                        External_id,
                                        Endpoint,
                                        3600,
                                        Timestamp
                                    ),
                                    (erlang:element(3, Provider))()
                                end
                            )
                        end
                    )
            end
    end.

-file("src/aws/credentials.gleam", 266).
?DOC(
    " Fully-explicit form — used by tests and callers that need a\n"
    " regional STS endpoint, custom file readers, or a pinned\n"
    " timestamp source for the SigV4 signer.\n"
).
-spec from_profile_assume_role_with(
    binary(),
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary(),
    binary(),
    fun(() -> {ok, binary()} | {error, nil}),
    fun(() -> {ok, binary()} | {error, nil}),
    fun(() -> binary())
) -> provider().
from_profile_assume_role_with(
    Profile_name,
    Send,
    Region,
    Endpoint,
    Credentials_reader,
    Config_reader,
    Timestamp
) ->
    {provider,
        <<<<"ProfileAssumeRole("/utf8, Profile_name/binary>>/binary, ")"/utf8>>,
        fun() ->
            fetch_from_profile_with_assume_role(
                Profile_name,
                Credentials_reader,
                Config_reader,
                Send,
                Region,
                Endpoint,
                Timestamp
            )
        end}.

-file("src/aws/credentials.gleam", 247).
?DOC(
    " Profile provider that auto-chains via STS AssumeRole when the\n"
    " requested profile carries `role_arn` / `source_profile`. The\n"
    " source profile must hold static keys; multi-hop chains are\n"
    " deferred. Falls through to the same static-keys path as\n"
    " `from_profile` when `role_arn` is absent, so a single chain\n"
    " entry covers both forms.\n"
).
-spec from_profile_assume_role(
    binary(),
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary()
) -> provider().
from_profile_assume_role(Profile_name, Send, Region) ->
    from_profile_assume_role_with(
        Profile_name,
        Send,
        Region,
        <<"https://sts.amazonaws.com/"/utf8>>,
        fun read_default_profile_file/0,
        fun read_default_config_file/0,
        fun aws_ffi:aws_timestamp/0
    ).

-file("src/aws/credentials.gleam", 545).
?DOC(
    " IMDSv2 provider with overridable endpoint and token TTL. Test stubs and\n"
    " fleet-specific deployments (e.g. when AWS_EC2_METADATA_SERVICE_ENDPOINT\n"
    " is set) use this form.\n"
).
-spec from_imds_with(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary(),
    integer()
) -> provider().
from_imds_with(Send, Endpoint, Token_ttl_seconds) ->
    Options = {options, Endpoint, Token_ttl_seconds},
    {provider,
        <<"IMDSv2"/utf8>>,
        fun() -> case aws@internal@providers@imds:fetch(Send, Options) of
                {ok, C} ->
                    {ok,
                        {credentials,
                            erlang:element(2, C),
                            erlang:element(3, C),
                            {some, erlang:element(4, C)},
                            {some, erlang:element(5, C)},
                            <<"IMDSv2"/utf8>>}};

                {error, {not_on_instance, Reason}} ->
                    {error, {not_configured, Reason}};

                {error, {failed, Reason@1}} ->
                    {error, {fetch_failed, Reason@1}}
            end end}.

-file("src/aws/credentials.gleam", 534).
?DOC(
    " IMDSv2 credentials provider. Performs the standard PUT-token / GET-role /\n"
    " GET-creds dance against the link-local metadata endpoint at\n"
    " `http://169.254.169.254` and parses the JSON credentials response.\n"
    "\n"
    " Failure of step 1 (the token PUT) is treated as `NotConfigured` so the\n"
    " chain quietly falls through to the next provider when we're not on EC2\n"
    " or Lambda. Failures past that point are `FetchFailed`.\n"
    "\n"
    " `send` is the HTTP transport — pass `aws/internal/http_send.default_send`\n"
    " in production, or a stub in tests.\n"
).
-spec from_imds(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()})
) -> provider().
from_imds(Send) ->
    from_imds_with(Send, <<"http://169.254.169.254"/utf8>>, 21600).

-file("src/aws/credentials.gleam", 665).
-spec read_file_string(binary()) -> {ok, binary()} | {error, nil}.
read_file_string(Path) ->
    gleam@result:'try'(
        aws_ffi:read_file(Path),
        fun(Bits) -> _pipe = gleam@bit_array:to_string(Bits),
            gleam@result:replace_error(_pipe, nil) end
    ).

-file("src/aws/credentials.gleam", 607).
?DOC(
    " ECS provider with the URL and auth token supplied explicitly. Useful when\n"
    " the env-resolution logic isn't a fit (e.g. a sidecar configures things\n"
    " programmatically).\n"
).
-spec from_ecs_with(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary(),
    gleam@option:option(binary())
) -> provider().
from_ecs_with(Send, Url, Auth_token) ->
    Options = {options, Url, Auth_token},
    {provider,
        <<"ECS"/utf8>>,
        fun() -> case aws@internal@providers@ecs:fetch(Send, Options) of
                {ok, C} ->
                    {ok,
                        {credentials,
                            erlang:element(2, C),
                            erlang:element(3, C),
                            erlang:element(4, C),
                            erlang:element(5, C),
                            <<"ECS"/utf8>>}};

                {error, {unreachable, Reason}} ->
                    {error, {not_configured, Reason}};

                {error, {failed, Reason@1}} ->
                    {error, {fetch_failed, Reason@1}}
            end end}.

-file("src/aws/credentials.gleam", 643).
-spec resolve_ecs_auth_token(
    fun((binary()) -> {ok, binary()} | {error, nil}),
    fun((binary()) -> {ok, binary()} | {error, nil})
) -> gleam@option:option(binary()).
resolve_ecs_auth_token(Lookup, Read_file) ->
    case Lookup(<<"AWS_CONTAINER_AUTHORIZATION_TOKEN"/utf8>>) of
        {ok, T} when T =/= <<""/utf8>> ->
            {some, T};

        _ ->
            case Lookup(<<"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"/utf8>>) of
                {ok, Path} when Path =/= <<""/utf8>> ->
                    case Read_file(Path) of
                        {ok, Contents} ->
                            case gleam@string:trim(Contents) of
                                <<""/utf8>> ->
                                    none;

                                T@1 ->
                                    {some, T@1}
                            end;

                        {error, _} ->
                            none
                    end;

                _ ->
                    none
            end
    end.

-file("src/aws/credentials.gleam", 630).
-spec resolve_ecs_url(fun((binary()) -> {ok, binary()} | {error, nil})) -> gleam@option:option(binary()).
resolve_ecs_url(Lookup) ->
    case Lookup(<<"AWS_CONTAINER_CREDENTIALS_FULL_URI"/utf8>>) of
        {ok, Full} when Full =/= <<""/utf8>> ->
            {some, Full};

        _ ->
            case Lookup(<<"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"/utf8>>) of
                {ok, Rel} when Rel =/= <<""/utf8>> ->
                    {some, <<"http://169.254.170.2"/utf8, Rel/binary>>};

                _ ->
                    none
            end
    end.

-file("src/aws/credentials.gleam", 586).
?DOC(
    " Like `from_ecs` but with injectable env-var lookup and file reader so\n"
    " tests can drive the provider without mutating real OS state.\n"
).
-spec from_ecs_with_env(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    fun((binary()) -> {ok, binary()} | {error, nil}),
    fun((binary()) -> {ok, binary()} | {error, nil})
) -> provider().
from_ecs_with_env(Send, Lookup, Read_file) ->
    Url = resolve_ecs_url(Lookup),
    Token = resolve_ecs_auth_token(Lookup, Read_file),
    case Url of
        {some, U} ->
            from_ecs_with(Send, U, Token);

        none ->
            {provider,
                <<"ECS"/utf8>>,
                fun() ->
                    {error,
                        {not_configured,
                            <<"no AWS_CONTAINER_CREDENTIALS_*_URI in environment"/utf8>>}}
                end}
    end.

-file("src/aws/credentials.gleam", 580).
?DOC(
    " ECS / EKS / Fargate container metadata provider. Resolves the metadata\n"
    " URL from the standard environment variables (AWS_CONTAINER_-\n"
    " CREDENTIALS_FULL_URI takes precedence; otherwise AWS_CONTAINER_-\n"
    " CREDENTIALS_RELATIVE_URI is appended to `http://169.254.170.2`). The\n"
    " `Authorization` header value is read from AWS_CONTAINER_AUTHORIZATION_-\n"
    " TOKEN (or _TOKEN_FILE, if set instead).\n"
    "\n"
    " If neither URI env var is set, the provider always returns\n"
    " `NotConfigured` so the chain falls through quietly.\n"
).
-spec from_ecs(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()})
) -> provider().
from_ecs(Send) ->
    from_ecs_with_env(Send, fun aws_ffi:get_env/1, fun read_file_string/1).

-file("src/aws/credentials.gleam", 758).
-spec do_fetch_web_identity(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary(),
    binary(),
    binary(),
    binary(),
    integer(),
    fun((binary()) -> {ok, binary()} | {error, nil})
) -> {ok, credentials()} | {error, provider_error()}.
do_fetch_web_identity(
    Send,
    Endpoint,
    Role_arn,
    Role_session_name,
    Token_file,
    Duration_seconds,
    Read_file
) ->
    gleam@result:'try'(
        begin
            _pipe = Read_file(Token_file),
            gleam@result:replace_error(
                _pipe,
                {fetch_failed,
                    <<"could not read web identity token from "/utf8,
                        Token_file/binary>>}
            )
        end,
        fun(Token) ->
            Options = {options,
                Endpoint,
                Role_arn,
                Role_session_name,
                gleam@string:trim(Token),
                Duration_seconds},
            case aws@internal@providers@sts_web_identity:fetch(Send, Options) of
                {ok, C} ->
                    {ok,
                        {credentials,
                            erlang:element(2, C),
                            erlang:element(3, C),
                            {some, erlang:element(4, C)},
                            {some, erlang:element(5, C)},
                            <<"WebIdentity"/utf8>>}};

                {error, {misconfigured, Reason}} ->
                    {error, {not_configured, Reason}};

                {error, {failed, Reason@1}} ->
                    {error, {fetch_failed, Reason@1}}
            end
        end
    ).

-file("src/aws/credentials.gleam", 727).
-spec fetch_web_identity(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    fun((binary()) -> {ok, binary()} | {error, nil}),
    fun((binary()) -> {ok, binary()} | {error, nil}),
    binary()
) -> {ok, credentials()} | {error, provider_error()}.
fetch_web_identity(Send, Lookup, Read_file, Endpoint) ->
    gleam@result:'try'(
        begin
            _pipe = Lookup(<<"AWS_WEB_IDENTITY_TOKEN_FILE"/utf8>>),
            gleam@result:replace_error(
                _pipe,
                {not_configured, <<"AWS_WEB_IDENTITY_TOKEN_FILE not set"/utf8>>}
            )
        end,
        fun(Token_file) ->
            gleam@result:'try'(
                begin
                    _pipe@1 = Lookup(<<"AWS_ROLE_ARN"/utf8>>),
                    gleam@result:replace_error(
                        _pipe@1,
                        {not_configured, <<"AWS_ROLE_ARN not set"/utf8>>}
                    )
                end,
                fun(Role_arn) ->
                    Role_session_name = case Lookup(
                        <<"AWS_ROLE_SESSION_NAME"/utf8>>
                    ) of
                        {ok, Name} when Name =/= <<""/utf8>> ->
                            Name;

                        _ ->
                            <<"aws-sdk-gleam-session"/utf8>>
                    end,
                    do_fetch_web_identity(
                        Send,
                        Endpoint,
                        Role_arn,
                        Role_session_name,
                        Token_file,
                        3600,
                        Read_file
                    )
                end
            )
        end
    ).

-file("src/aws/credentials.gleam", 693).
?DOC(" Injectable env / file reader variant for tests.\n").
-spec from_web_identity_with_env(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    fun((binary()) -> {ok, binary()} | {error, nil}),
    fun((binary()) -> {ok, binary()} | {error, nil})
) -> provider().
from_web_identity_with_env(Send, Lookup, Read_file) ->
    {provider,
        <<"WebIdentity"/utf8>>,
        fun() ->
            fetch_web_identity(
                Send,
                Lookup,
                Read_file,
                <<"https://sts.amazonaws.com/"/utf8>>
            )
        end}.

-file("src/aws/credentials.gleam", 684).
?DOC(
    " IRSA / STS Web Identity provider. Reads the token from\n"
    " `AWS_WEB_IDENTITY_TOKEN_FILE` *each fetch* (IRSA rotates the file), reads\n"
    " `AWS_ROLE_ARN` once at construction, and POSTs to STS.\n"
).
-spec from_web_identity(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()})
) -> provider().
from_web_identity(Send) ->
    from_web_identity_with_env(
        Send,
        fun aws_ffi:get_env/1,
        fun read_file_string/1
    ).

-file("src/aws/credentials.gleam", 705).
?DOC(
    " Fully-explicit variant — caller provides every parameter. Used by tests\n"
    " to point at a stub endpoint, and by callers who configure programmatically.\n"
).
-spec from_web_identity_with(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary(),
    binary(),
    binary(),
    binary(),
    integer(),
    fun((binary()) -> {ok, binary()} | {error, nil})
) -> provider().
from_web_identity_with(
    Send,
    Endpoint,
    Role_arn,
    Role_session_name,
    Token_file,
    Duration_seconds,
    Read_file
) ->
    {provider,
        <<"WebIdentity"/utf8>>,
        fun() ->
            do_fetch_web_identity(
                Send,
                Endpoint,
                Role_arn,
                Role_session_name,
                Token_file,
                Duration_seconds,
                Read_file
            )
        end}.

-file("src/aws/credentials.gleam", 851).
-spec wrap_sso_result(
    {ok, aws@internal@providers@sso:sso_credentials()} |
        {error, aws@internal@providers@sso:error()}
) -> {ok, credentials()} | {error, provider_error()}.
wrap_sso_result(Result) ->
    case Result of
        {ok, C} ->
            {ok,
                {credentials,
                    erlang:element(2, C),
                    erlang:element(3, C),
                    {some, erlang:element(4, C)},
                    {some, erlang:element(5, C)},
                    <<"SSO"/utf8>>}};

        {error, {unreachable, Reason}} ->
            {error, {not_configured, Reason}};

        {error, {failed, Reason@1}} ->
            {error, {fetch_failed, Reason@1}}
    end.

-file("src/aws/credentials.gleam", 808).
?DOC(
    " SSO credentials provider. Consumes a cached SSO access token (produced\n"
    " by `aws sso login`) and exchanges it at the portal for short-lived\n"
    " credentials.\n"
    "\n"
    " `from_sso_with` is the explicit form — used by tests and by callers that\n"
    " resolve their own session / role configuration.\n"
).
-spec from_sso_with(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary(),
    binary(),
    binary(),
    binary()
) -> provider().
from_sso_with(Send, Region, Account_id, Role_name, Access_token) ->
    Options = {options,
        Region,
        Account_id,
        Role_name,
        Access_token,
        aws@internal@providers@sso:default_endpoint(Region)},
    {provider,
        <<"SSO"/utf8>>,
        fun() ->
            wrap_sso_result(aws@internal@providers@sso:fetch(Send, Options))
        end}.

-file("src/aws/credentials.gleam", 830).
?DOC(
    " Same shape as `from_sso_with` but with an overridable portal endpoint.\n"
    " Tests aim a stub server at the portal path and pass it here.\n"
).
-spec from_sso_with_endpoint(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary(),
    binary(),
    binary(),
    binary(),
    binary()
) -> provider().
from_sso_with_endpoint(
    Send,
    Region,
    Account_id,
    Role_name,
    Access_token,
    Endpoint
) ->
    Options = {options, Region, Account_id, Role_name, Access_token, Endpoint},
    {provider,
        <<"SSO"/utf8>>,
        fun() ->
            wrap_sso_result(aws@internal@providers@sso:fetch(Send, Options))
        end}.

-file("src/aws/credentials.gleam", 1060).
-spec read_sso_cache_file(binary()) -> {ok, binary()} | {error, nil}.
read_sso_cache_file(Filename) ->
    gleam@result:'try'(
        aws_ffi:get_env(<<"HOME"/utf8>>),
        fun(Home) ->
            Path = <<<<Home/binary, "/.aws/sso/cache/"/utf8>>/binary,
                Filename/binary>>,
            gleam@result:'try'(
                aws_ffi:read_file(Path),
                fun(Bits) -> _pipe = gleam@bit_array:to_string(Bits),
                    gleam@result:replace_error(_pipe, nil) end
            )
        end
    ).

-file("src/aws/credentials.gleam", 1047).
-spec extract_access_token(binary()) -> {ok, binary()} | {error, nil}.
extract_access_token(Json_text) ->
    aws@internal@text_scan:json_string_after_key(
        Json_text,
        <<"accessToken"/utf8>>
    ).

-file("src/aws/credentials.gleam", 1038).
?DOC(
    " Look up a required INI property, returning `Error(\"no <key>\")` if\n"
    " the key is absent. Used by both SSO resolvers; the consistent\n"
    " error format makes the \"is this an SSO profile at all?\" check\n"
    " readable at the call site.\n"
).
-spec require_property(
    gleam@dict:dict(binary(), gleam@dict:dict(binary(), binary())),
    binary(),
    binary()
) -> {ok, binary()} | {error, binary()}.
require_property(Config, Section, Key) ->
    _pipe = aws@internal@ini:get_property(Config, Section, Key),
    gleam@result:replace_error(_pipe, <<"no "/utf8, Key/binary>>).

-file("src/aws/credentials.gleam", 1013).
-spec resolve_sso_config_legacy(
    gleam@dict:dict(binary(), gleam@dict:dict(binary(), binary())),
    binary()
) -> {ok, sso_config()} | {error, binary()}.
resolve_sso_config_legacy(Config, Section) ->
    gleam@result:'try'(
        require_property(Config, Section, <<"sso_start_url"/utf8>>),
        fun(Start_url) ->
            gleam@result:'try'(
                require_property(Config, Section, <<"sso_region"/utf8>>),
                fun(Region) ->
                    gleam@result:'try'(
                        require_property(
                            Config,
                            Section,
                            <<"sso_account_id"/utf8>>
                        ),
                        fun(Account_id) ->
                            gleam@result:'try'(
                                require_property(
                                    Config,
                                    Section,
                                    <<"sso_role_name"/utf8>>
                                ),
                                fun(Role_name) ->
                                    {ok,
                                        {sso_config,
                                            Region,
                                            Account_id,
                                            Role_name,
                                            aws_ffi:sha1_hex(Start_url),
                                            <<<<"start URL '"/utf8,
                                                    Start_url/binary>>/binary,
                                                "'"/utf8>>}}
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/aws/credentials.gleam", 985).
-spec resolve_sso_config_modern(
    gleam@dict:dict(binary(), gleam@dict:dict(binary(), binary())),
    binary()
) -> {ok, sso_config()} | {error, binary()}.
resolve_sso_config_modern(Config, Section) ->
    gleam@result:'try'(
        require_property(Config, Section, <<"sso_session"/utf8>>),
        fun(Session) ->
            gleam@result:'try'(
                require_property(Config, Section, <<"sso_account_id"/utf8>>),
                fun(Account_id) ->
                    gleam@result:'try'(
                        require_property(
                            Config,
                            Section,
                            <<"sso_role_name"/utf8>>
                        ),
                        fun(Role_name) ->
                            Session_section = <<"sso-session "/utf8,
                                Session/binary>>,
                            gleam@result:'try'(
                                begin
                                    _pipe = require_property(
                                        Config,
                                        Session_section,
                                        <<"sso_region"/utf8>>
                                    ),
                                    gleam@result:lazy_or(
                                        _pipe,
                                        fun() ->
                                            require_property(
                                                Config,
                                                Section,
                                                <<"sso_region"/utf8>>
                                            )
                                        end
                                    )
                                end,
                                fun(Region) ->
                                    {ok,
                                        {sso_config,
                                            Region,
                                            Account_id,
                                            Role_name,
                                            aws_ffi:sha1_hex(Session),
                                            <<<<"session '"/utf8,
                                                    Session/binary>>/binary,
                                                "'"/utf8>>}}
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/aws/credentials.gleam", 977).
?DOC(
    " Build an `SsoConfig` from the profile's settings, preferring the\n"
    " modern `sso_session` shape and falling back to the legacy\n"
    " `sso_start_url` shape. The reason surfaced on failure is always\n"
    " the legacy branch's (it runs second through `lazy_or`); the\n"
    " modern branch's \"no sso_session\" is the trivial case the chain\n"
    " expects when a profile simply isn't an SSO profile, so swallowing\n"
    " it is the right choice.\n"
).
-spec resolve_sso_config(
    gleam@dict:dict(binary(), gleam@dict:dict(binary(), binary())),
    binary()
) -> {ok, sso_config()} | {error, binary()}.
resolve_sso_config(Config, Section) ->
    _pipe = resolve_sso_config_modern(Config, Section),
    gleam@result:lazy_or(
        _pipe,
        fun() -> resolve_sso_config_legacy(Config, Section) end
    ).

-file("src/aws/credentials.gleam", 909).
-spec resolve_and_fetch_sso(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary(),
    fun(() -> {ok, binary()} | {error, nil}),
    fun((binary()) -> {ok, binary()} | {error, nil})
) -> {ok, credentials()} | {error, provider_error()}.
resolve_and_fetch_sso(Send, Profile, Config_reader, Cache_reader) ->
    gleam@result:'try'(
        begin
            _pipe = Config_reader(),
            gleam@result:replace_error(
                _pipe,
                {not_configured, <<"~/.aws/config not readable"/utf8>>}
            )
        end,
        fun(Config_text) ->
            gleam@result:'try'(
                begin
                    _pipe@1 = aws@internal@ini:parse(Config_text),
                    gleam@result:map_error(
                        _pipe@1,
                        fun(E) ->
                            {fetch_failed,
                                <<"config parse error at line "/utf8,
                                    (erlang:integer_to_binary(
                                        erlang:element(2, E)
                                    ))/binary>>}
                        end
                    )
                end,
                fun(Config) ->
                    Section = case Profile of
                        <<"default"/utf8>> ->
                            <<"default"/utf8>>;

                        Other ->
                            <<"profile "/utf8, Other/binary>>
                    end,
                    gleam@result:'try'(
                        begin
                            _pipe@2 = resolve_sso_config(Config, Section),
                            gleam@result:map_error(
                                _pipe@2,
                                fun(Reason) ->
                                    {not_configured,
                                        <<<<<<"profile '"/utf8, Profile/binary>>/binary,
                                                "' is not an SSO profile: "/utf8>>/binary,
                                            Reason/binary>>}
                                end
                            )
                        end,
                        fun(Sso_cfg) ->
                            Cache_filename = <<(erlang:element(5, Sso_cfg))/binary,
                                ".json"/utf8>>,
                            gleam@result:'try'(
                                begin
                                    _pipe@3 = Cache_reader(Cache_filename),
                                    gleam@result:replace_error(
                                        _pipe@3,
                                        {not_configured,
                                            <<<<"no SSO token cache for "/utf8,
                                                    (erlang:element(6, Sso_cfg))/binary>>/binary,
                                                " — run `aws sso login`"/utf8>>}
                                    )
                                end,
                                fun(Cache_text) ->
                                    gleam@result:'try'(
                                        begin
                                            _pipe@4 = extract_access_token(
                                                Cache_text
                                            ),
                                            gleam@result:replace_error(
                                                _pipe@4,
                                                {fetch_failed,
                                                    <<<<"SSO token cache for "/utf8,
                                                            (erlang:element(
                                                                6,
                                                                Sso_cfg
                                                            ))/binary>>/binary,
                                                        " is missing accessToken"/utf8>>}
                                            )
                                        end,
                                        fun(Access_token) ->
                                            wrap_sso_result(
                                                aws@internal@providers@sso:fetch(
                                                    Send,
                                                    {options,
                                                        erlang:element(
                                                            2,
                                                            Sso_cfg
                                                        ),
                                                        erlang:element(
                                                            3,
                                                            Sso_cfg
                                                        ),
                                                        erlang:element(
                                                            4,
                                                            Sso_cfg
                                                        ),
                                                        Access_token,
                                                        aws@internal@providers@sso:default_endpoint(
                                                            erlang:element(
                                                                2,
                                                                Sso_cfg
                                                            )
                                                        )}
                                                )
                                            )
                                        end
                                    )
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/aws/credentials.gleam", 883).
?DOC(" Injectable variant for tests.\n").
-spec from_sso_with_env(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary(),
    fun(() -> {ok, binary()} | {error, nil}),
    fun((binary()) -> {ok, binary()} | {error, nil})
) -> provider().
from_sso_with_env(Send, Profile, Config_reader, Cache_reader) ->
    {provider,
        <<<<"SSO("/utf8, Profile/binary>>/binary, ")"/utf8>>,
        fun() ->
            resolve_and_fetch_sso(Send, Profile, Config_reader, Cache_reader)
        end}.

-file("src/aws/credentials.gleam", 873).
?DOC(
    " Production SSO provider. Reads the named profile from `~/.aws/config`,\n"
    " pulls the cached SSO access token from `~/.aws/sso/cache/<sha1>.json`,\n"
    " and exchanges it at the portal. The cache filename is `sha1(session-or-\n"
    " start-url)` per the AWS CLI convention.\n"
).
-spec from_sso(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary()
) -> provider().
from_sso(Send, Profile) ->
    from_sso_with_env(
        Send,
        Profile,
        fun read_default_config_file/0,
        fun read_sso_cache_file/1
    ).

-file("src/aws/credentials.gleam", 1089).
-spec wrap_process_result(
    {ok, aws@internal@providers@process:process_credentials()} |
        {error, aws@internal@providers@process:error()}
) -> {ok, credentials()} | {error, provider_error()}.
wrap_process_result(Result) ->
    case Result of
        {ok, C} ->
            {ok,
                {credentials,
                    erlang:element(2, C),
                    erlang:element(3, C),
                    erlang:element(4, C),
                    erlang:element(5, C),
                    <<"Process"/utf8>>}};

        {error, {launch_failed, Reason}} ->
            {error, {not_configured, Reason}};

        {error, {bad_output, Reason@1}} ->
            {error, {fetch_failed, Reason@1}}
    end.

-file("src/aws/credentials.gleam", 1080).
?DOC(
    " Same shape but with an injectable runner so tests can drive scripted\n"
    " stdout/exit values without spawning real processes.\n"
).
-spec from_process_with_runner(
    binary(),
    fun((binary(), list(binary())) -> {ok, {integer(), bitstring()}} |
        {error, nil})
) -> provider().
from_process_with_runner(Command, Runner) ->
    {provider,
        <<"Process"/utf8>>,
        fun() ->
            wrap_process_result(
                aws@internal@providers@process:fetch(Runner, Command)
            )
        end}.

-file("src/aws/credentials.gleam", 1074).
?DOC(
    " Credential-process provider. Runs the configured command and parses its\n"
    " stdout as the AWS credential-process JSON.\n"
    "\n"
    " `from_process_with_command` takes a command line literally; tests and\n"
    " programmatic configs use this form.\n"
).
-spec from_process_with_command(binary()) -> provider().
from_process_with_command(Command) ->
    from_process_with_runner(Command, fun aws_ffi:run_process/2).

-file("src/aws/credentials.gleam", 1158).
-spec lookup_credential_process(
    binary(),
    fun(() -> {ok, binary()} | {error, nil}),
    boolean()
) -> {ok, binary()} | {error, nil}.
lookup_credential_process(Profile, Reader, In_config) ->
    gleam@result:'try'(
        Reader(),
        fun(Text) ->
            gleam@result:'try'(
                begin
                    _pipe = aws@internal@ini:parse(Text),
                    gleam@result:replace_error(_pipe, nil)
                end,
                fun(Parsed) ->
                    Section = case {In_config, Profile} of
                        {true, <<"default"/utf8>>} ->
                            <<"default"/utf8>>;

                        {true, Other} ->
                            <<"profile "/utf8, Other/binary>>;

                        {false, Other@1} ->
                            Other@1
                    end,
                    aws@internal@ini:get_property(
                        Parsed,
                        Section,
                        <<"credential_process"/utf8>>
                    )
                end
            )
        end
    ).

-file("src/aws/credentials.gleam", 1134).
-spec resolve_and_run_process(
    binary(),
    fun(() -> {ok, binary()} | {error, nil}),
    fun(() -> {ok, binary()} | {error, nil}),
    fun((binary(), list(binary())) -> {ok, {integer(), bitstring()}} |
        {error, nil})
) -> {ok, credentials()} | {error, provider_error()}.
resolve_and_run_process(Profile, Config_reader, Credentials_reader, Runner) ->
    Command = begin
        _pipe = lookup_credential_process(Profile, Config_reader, true),
        gleam@result:'or'(
            _pipe,
            lookup_credential_process(Profile, Credentials_reader, false)
        )
    end,
    gleam@result:'try'(
        begin
            _pipe@1 = Command,
            gleam@result:replace_error(
                _pipe@1,
                {not_configured,
                    <<<<"profile '"/utf8, Profile/binary>>/binary,
                        "' has no credential_process setting"/utf8>>}
            )
        end,
        fun(Cmd) ->
            wrap_process_result(
                aws@internal@providers@process:fetch(Runner, Cmd)
            )
        end
    ).

-file("src/aws/credentials.gleam", 1123).
?DOC(" Injectable variant for tests.\n").
-spec from_process_with_env(
    binary(),
    fun(() -> {ok, binary()} | {error, nil}),
    fun(() -> {ok, binary()} | {error, nil}),
    fun((binary(), list(binary())) -> {ok, {integer(), bitstring()}} |
        {error, nil})
) -> provider().
from_process_with_env(Profile, Config_reader, Credentials_reader, Runner) ->
    {provider,
        <<<<"Process("/utf8, Profile/binary>>/binary, ")"/utf8>>,
        fun() ->
            resolve_and_run_process(
                Profile,
                Config_reader,
                Credentials_reader,
                Runner
            )
        end}.

-file("src/aws/credentials.gleam", 1113).
?DOC(
    " Production credential-process provider. Reads `credential_process` from\n"
    " the named profile in `~/.aws/config` (or `~/.aws/credentials` for the\n"
    " `[default]` profile only — both files are checked).\n"
).
-spec from_process(binary()) -> provider().
from_process(Profile) ->
    from_process_with_env(
        Profile,
        fun read_default_config_file/0,
        fun read_default_profile_file/0,
        fun aws_ffi:run_process/2
    ).

-file("src/aws/credentials.gleam", 1208).
?DOC(
    " `from_aws_cli` with an injectable runner so tests don't actually spawn\n"
    " `aws`.\n"
).
-spec from_aws_cli_with(
    binary(),
    fun((binary(), list(binary())) -> {ok, {integer(), bitstring()}} |
        {error, nil})
) -> provider().
from_aws_cli_with(Profile, Runner) ->
    Command = <<<<"aws configure export-credentials --profile "/utf8,
            Profile/binary>>/binary,
        " --format process"/utf8>>,
    {provider,
        <<<<"AwsCli("/utf8, Profile/binary>>/binary, ")"/utf8>>,
        fun() -> case aws@internal@providers@process:fetch(Runner, Command) of
                {ok, C} ->
                    {ok,
                        {credentials,
                            erlang:element(2, C),
                            erlang:element(3, C),
                            erlang:element(4, C),
                            erlang:element(5, C),
                            <<<<"AwsCli("/utf8, Profile/binary>>/binary,
                                ")"/utf8>>}};

                {error, {launch_failed, Reason}} ->
                    {error, {not_configured, Reason}};

                {error, {bad_output, Reason@1}} ->
                    {error, {fetch_failed, Reason@1}}
            end end}.

-file("src/aws/credentials.gleam", 1202).
?DOC(
    " Use the AWS CLI (`aws configure export-credentials`) to resolve\n"
    " credentials for a profile. Covers any auth flow the CLI supports — SSO,\n"
    " IRSA, `login_session`, anything we haven't natively implemented yet.\n"
    "\n"
    " The CLI's `--format process` output is the same shape as\n"
    " `credential_process` (`Version: 1`, `AccessKeyId`, etc.), so this is\n"
    " effectively a thin wrapper that runs the right command and feeds the\n"
    " output through the existing `credential_process` decoder.\n"
    "\n"
    " Specifically a deliberate alternative to a native `login_session`\n"
    " provider: the upstream Go SDK's `credentials/logincreds` uses **DPoP**\n"
    " (RFC 9449) — every portal request needs a JWT signed with an ECDSA P-256\n"
    " private key from the local cache. Implementing that natively would add\n"
    " JWK parsing, JWS signing, and a new crypto FFI, plus the cache file\n"
    " schema isn't published outside the Go implementation. Until we take on\n"
    " that work, shelling out to the AWS CLI is the practical bridge.\n"
    "\n"
    " Requires AWS CLI v2 (`aws configure export-credentials` was added in\n"
    " 2022). Returns `NotConfigured` if the binary isn't on PATH or the\n"
    " profile doesn't exist; `FetchFailed` if the CLI exits non-zero or\n"
    " emits malformed JSON.\n"
).
-spec from_aws_cli(binary()) -> provider().
from_aws_cli(Profile) ->
    from_aws_cli_with(Profile, fun aws_ffi:run_process/2).

-file("src/aws/credentials.gleam", 1252).
?DOC(
    " Provider that wraps a source provider with an STS `AssumeRole` call.\n"
    "\n"
    " Fetch order on every call:\n"
    "   1. The wrapped `source` provider resolves \"outer\" credentials.\n"
    "   2. Those credentials sign a `AssumeRole` request to STS, which\n"
    "      hands back temporary credentials for the target role.\n"
    "\n"
    " `region` is what STS signs against; the global endpoint accepts any\n"
    " region so `\"us-east-1\"` is a safe default.\n"
    "\n"
    " Use this when your profile carries a `role_arn` / `source_profile`\n"
    " chain, or when you need a programmatic assume-role hop without\n"
    " editing your shared config.\n"
).
-spec from_assume_role(
    provider(),
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary(),
    binary(),
    binary(),
    gleam@option:option(binary())
) -> provider().
from_assume_role(Source, Send, Region, Role_arn, Role_session_name, External_id) ->
    from_assume_role_with(
        Source,
        Send,
        Region,
        Role_arn,
        Role_session_name,
        External_id,
        <<"https://sts.amazonaws.com/"/utf8>>,
        3600,
        fun aws_ffi:aws_timestamp/0
    ).

-file("src/aws/credentials.gleam", 1368).
?DOC(
    " Injectable variant of `default_chain`. Every OS-touching seam (env-var\n"
    " lookup, file reading, OS-process spawning, HTTP send) is a parameter, so a\n"
    " test can drive the chain end-to-end without touching real env or filesystem.\n"
    "\n"
    " `send` is the HTTP transport used by web-identity, SSO, and ECS; `imds_send`\n"
    " is the short-timeout variant used by IMDS specifically — they're separate\n"
    " arguments because the production wiring picks distinct senders for them.\n"
).
-spec default_chain_with(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary(),
    fun((binary()) -> {ok, binary()} | {error, nil}),
    fun((binary()) -> {ok, binary()} | {error, nil}),
    fun((binary(), list(binary())) -> {ok, {integer(), bitstring()}} |
        {error, nil})
) -> provider().
default_chain_with(Send, Imds_send, Profile, Env, Read_file, Runner) ->
    Config_reader = fun() ->
        gleam@result:'try'(
            Env(<<"HOME"/utf8>>),
            fun(Home) -> Read_file(<<Home/binary, "/.aws/config"/utf8>>) end
        )
    end,
    Credentials_reader = fun() ->
        gleam@result:'try'(
            Env(<<"HOME"/utf8>>),
            fun(Home@1) ->
                Read_file(<<Home@1/binary, "/.aws/credentials"/utf8>>)
            end
        )
    end,
    Sso_cache_reader = fun(Filename) ->
        gleam@result:'try'(
            Env(<<"HOME"/utf8>>),
            fun(Home@2) ->
                Read_file(
                    <<<<Home@2/binary, "/.aws/sso/cache/"/utf8>>/binary,
                        Filename/binary>>
                )
            end
        )
    end,
    chain(
        [from_environment_with(Env),
            from_web_identity_with_env(Send, Env, Read_file),
            from_sso_with_env(Send, Profile, Config_reader, Sso_cache_reader),
            from_profile_with(Profile, Credentials_reader, Config_reader),
            from_process_with_env(
                Profile,
                Config_reader,
                Credentials_reader,
                Runner
            ),
            from_aws_cli_with(Profile, Runner),
            from_ecs_with_env(Send, Env, Read_file),
            from_imds(Imds_send)]
    ).

-file("src/aws/credentials.gleam", 1350).
?DOC(
    " Standard AWS credential-provider chain, in the precedence order other AWS\n"
    " SDKs use:\n"
    "\n"
    "   1. Environment variables (`AWS_ACCESS_KEY_ID` and friends)\n"
    "   2. AssumeRoleWithWebIdentity / IRSA (`AWS_WEB_IDENTITY_TOKEN_FILE`)\n"
    "   3. SSO session, via `~/.aws/config` + the cached SSO token\n"
    "   4. Shared credentials file (`~/.aws/credentials`)\n"
    "   5. `credential_process` from the named profile\n"
    "   6. `aws configure export-credentials` (covers Identity Center / SSO\n"
    "      sessions and other CLI-only auth flows when the native providers\n"
    "      don't recognise the profile shape)\n"
    "   7. ECS container metadata (`AWS_CONTAINER_CREDENTIALS_*_URI`)\n"
    "   8. EC2 IMDSv2\n"
    "\n"
    " The returned `Provider` is the bare chain — it does not cache. Wrap it in\n"
    " `aws/internal/credentials_cache.start_default` to get the cache + refresh\n"
    " behaviour every long-running process wants.\n"
    "\n"
    " `profile` selects which profile name is used by the profile, SSO, and\n"
    " credential_process branches (they all share the AWS-CLI profile concept).\n"
    " Pass `\"default\"` to mimic the AWS CLI's default behaviour.\n"
).
-spec default_chain(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    binary()
) -> provider().
default_chain(Send, Profile) ->
    default_chain_with(
        Send,
        fun aws@internal@http_send:imds_send/1,
        Profile,
        fun aws_ffi:get_env/1,
        fun read_file_string/1,
        fun aws_ffi:run_process/2
    ).