EndPointBlank.AccessTokens (end_point_blank_elixir v0.9.0)

Copy Markdown View Source

In-process cache of this node's access tokens, one per application environment.

A token is cached under the canonical base URL intake resolved the request to -- not under the URL the caller supplied. A caller asks for the URL it is about to call; intake answers with the base URL of the environment that URL belongs to, and subsequent calls anywhere under that base URL reuse the entry. A node that calls several targets therefore holds several tokens.

Lookup is a plain exact-or-path-prefix comparison, with the longest match winning. The SDK deliberately does not normalize: intake owns that rule, and a miss costs one extra request rather than a wrong answer.

All reads and writes go through this GenServer's mailbox, so they are fully serialized -- unlike the Python and Ruby ports, there is no lock-free fast path here for a concurrent write to race, so this holds a plain map and mutates it directly. Do not add copy-on-write; there is nothing here for it to protect.

Tokens are proactively refreshed when they are within two minutes of expiry to avoid serving one that dies in flight -- an expired token can never be revived, only replaced.

Why a mint failed

token/1 answers nil for every failure; token_result/1 answers {:error, reason} with the reason for that very mint, and is what EndPointBlank.Authorization.header/1 uses to refuse an outbound call with a message that says why. The detail matters: intake answers 401 for a credential it has rejected, and that is permanent until a human re-issues the credential, where a 5xx or a refused connection will likely be gone on the next call. last_failure/1 reports the last one for a given URL, so a caller that wants to stop hammering intake -- or alarm -- can tell those apart. See EndPointBlank.Commands.GenerateAccessToken for the full taxonomy.

Summary

Types

Why the last mint for a URL failed, or nil if the last one succeeded (or none has been attempted).

Functions

Returns a specification to start this module under a supervisor.

Discards every held token, and every recorded failure.

Returns true if a token covering base_url is held and not about to expire.

Discards a held token, but only if it is still the one the caller had.

Returns why the last mint attempted for base_url failed, or nil if the last one succeeded or none has been made.

Returns a valid access token for base_url, minting one if no usable entry covers it.

Like token/1, but says why no token could be produced.

Types

failure()

Why the last mint for a URL failed, or nil if the last one succeeded (or none has been attempted).

Exactly EndPointBlank.Commands.GenerateAccessToken.failure/0, so every SDK reports the same set: :credential_rejected and {:request_rejected, status} are permanent -- retrying changes nothing until the credential is re-issued or the environment is registered -- while {:server_error, status} and {:transport_error, reason} are transient.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

clear()

Discards every held token, and every recorded failure.

exists?(base_url)

Returns true if a token covering base_url is held and not about to expire.

Userinfo, query and fragment are removed first, as for token/1.

invalidate(stale_token)

Discards a held token, but only if it is still the one the caller had.

Every request in flight when a token is rejected reports the same stale value. Only the first of them should cause a mint — the rest are holding a token that has already been replaced, and clearing on their behalf would discard a good token and stampede intake.

The lookup is by token value because a rejected caller has a token, not a base_url.

last_failure(base_url)

@spec last_failure(term()) :: failure() | nil

Returns why the last mint attempted for base_url failed, or nil if the last one succeeded or none has been made.

Pass the same URL you passed to token/1: the record is kept under the URL the caller asked about (with its userinfo, query and fragment removed), not under the canonical base URL intake resolved it to, because on a failure there is no resolved base URL to key it under.

Only the 64 most recently failed URLs are kept, so this answers nil for a URL whose failure has since been pushed out by newer ones. Ask about a URL you just called and it will be there.

case AccessTokens.token(url) do
  nil ->
    case AccessTokens.last_failure(url) do
      :credential_rejected -> alarm("re-issue the EndPointBlank credential")
      {:request_rejected, status} -> alarm("intake refused the request: #{status}")
      _transient -> :ok
    end

  token ->
    {:ok, token}
end

See failure/0 for the shapes and which of them are permanent. Answers nil -- never raises -- for a URL nothing was recorded under, including any argument that is not a binary.

A successful mint clears the record for the URL it was asked about and for every URL the resolved base URL covers, so this cannot go on reporting a failure that has since been fixed.

start_link(opts)

token(base_url)

@spec token(term()) :: String.t() | nil

Returns a valid access token for base_url, minting one if no usable entry covers it.

base_url is the URL you are about to call. Its userinfo, query and fragment are removed first (EndPointBlank.OutboundUrl.strip/1): they are never sent to intake, logged, or used as a cache or failure key. The scheme and host are lowercased and a default or empty port is dropped; the path is sent as written. intake normalizes it and matches it against registered base URLs by longest path prefix. A URL that is not a non-empty string, or that OutboundUrl.strip/1 refuses (not an absolute http or https URL with a host, or a port outside 1..65535), is refused without asking intake.

Returns nil rather than raising if a token cannot be produced -- which includes a response that carried a token but no base_url (nothing to cache it under) as well as the cache failing to answer in time. A nil means the call must not be made: there is no fallback credential for a provider (sc-1469).

Use token_result/1 to get the reason for this mint along with it, or last_failure/1 to find out afterwards which kind of failure a nil was.

token_result(base_url)

@spec token_result(term()) ::
  {:ok, String.t()}
  | {:error,
     failure()
     | :token_cache_unavailable
     | :missing_base_url
     | :invalid_base_url
     | :missing_credentials
     | {:unexpected, term()}}

Like token/1, but says why no token could be produced.

Returns {:ok, token} or {:error, reason}, where reason is a failure/0 from this very mint -- not whatever last_failure/1 holds by the time the caller asks, which a concurrent caller's mint may already have replaced or cleared -- or :token_cache_unavailable when this cache did not answer at all (it was not running, or a mint against a hung intake outlasted the 20000 ms the call allows). A base_url that is not a non-empty string answers {:error, :missing_base_url}, and one that OutboundUrl.strip/1 refuses (not an absolute http or https URL with a host, or a port outside 1..65535) {:error, :invalid_base_url}, without asking intake.

Two more answers are not a mint intake failed, so neither is recorded for last_failure/1 nor drops a held entry: {:error, :missing_credentials} when client_id or client_secret is not configured (nothing is sent), and {:error, {:unexpected, reason}} when minting raised, threw, or the HTTP client answered something that is not a transport failure. reason is what was raised, {kind, term} for a throw or exit, or the client's error term. Never raises.