-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", 72). ?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", 85). -spec try_each(list(provider()), list({binary(), provider_error()})) -> {ok, credentials()} | {error, provider_error()}. try_each(Providers, Attempts) -> case Providers of [] -> {error, {chain_exhausted, lists:reverse(Attempts)}}; [P | Rest] -> case (erlang:element(3, P))() of {ok, Credentials} -> {ok, Credentials}; {error, Reason} -> try_each(Rest, [{erlang:element(2, P), Reason} | Attempts]) end end. -file("src/aws/credentials.gleam", 81). ?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", 102). ?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", 127). -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", 116). ?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", 123). ?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", 360). -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", 318). ?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", 297). -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", 263). -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", 186). ?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", 1018). -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 = <>, 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", 256). -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 = <>, 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", 198). ?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", 1240). ?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", 407). ?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", 231). ?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", 212). ?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", 510). ?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", 499). ?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", 630). -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", 572). ?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", 608). -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", 595). -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", 551). ?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", 545). ?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", 723). -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", 692). -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", 658). ?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", 649). ?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", 670). ?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", 816). -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", 773). ?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", 795). ?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", 1025). -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 = <<<>/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", 1012). -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", 1003). ?DOC( " Look up a required INI property, returning `Error(\"no \")` 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", 978). -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", 950). -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", 942). ?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", 874). -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", 848). ?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", 838). ?DOC( " Production SSO provider. Reads the named profile from `~/.aws/config`,\n" " pulls the cached SSO access token from `~/.aws/sso/cache/.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", 1054). -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", 1045). ?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", 1039). ?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", 1123). -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", 1099). -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", 1088). ?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", 1078). ?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", 1173). ?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", 1167). ?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", 1217). ?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", 1333). ?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(<>) end ) end, Credentials_reader = fun() -> gleam@result:'try'( Env(<<"HOME"/utf8>>), fun(Home@1) -> Read_file(<>) end ) end, Sso_cache_reader = fun(Filename) -> gleam@result:'try'( Env(<<"HOME"/utf8>>), fun(Home@2) -> Read_file( <<<>/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", 1315). ?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 ).