-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
).