Singleton configuration store for the EndPointBlank library.
Update via EndPointBlank.configure/1.
Reads do not go through a process
This config is read-mostly to an extreme degree: it is written by
EndPointBlank.configure/1, usually once at boot, and read on every inbound
request (the authorization plug, the version finder) and every outbound write
(each writer, the masking layer, every URL builder). The two sides are
therefore stored differently.
Writes go through an Agent, which is the writer of record. It serialises
concurrent update/1 calls so each one's read-modify-write is atomic, and it
owns the ETS table below, so the config still dies with the process — a
restart starts from a blank %EndPointBlank.Config{} exactly as it did when
the Agent was the only store.
Reads go straight to that table: a :protected, read_concurrency set that
only the Agent writes. get/0 is a lock-free lookup performed in the calling
process, so config reads no longer serialise behind one mailbox — and, the
reason this matters more than the speed, a slow, wedged or restarting Agent
can no longer take its readers with it. Agent.get/2 carries a 5000 ms
default timeout and a timeout exits the caller: until sc-350, every
authorization plug call and every writer was one busy Agent away from dying.
sc-331 had to catch that exit in EndPointBlank.Writers.DelayedWriter for
precisely this reason.
There is deliberately no fallback
If the store is unavailable, get/0 raises. It does not quietly return a
default %EndPointBlank.Config{}: that would authorize requests against
nil credentials and ship telemetry to the public default base URL because a
process happened to be down, which is a far worse failure than a raise and
exactly the silent fallback this library refuses.
Environment variables
Several settings also fall back to ENDPOINTBLANK_* environment
variables when not explicitly configured, in order to support
Rails-free (and Elixir-config-free) deployments. Precedence for each
such setting is: explicit value set via EndPointBlank.configure/1 >
System.get_env/1 > built-in default. The environment variable is
read at getter time (not cached), so it can be changed at runtime.
Summary
Functions
Returns a specification to start this module under a supervisor.
The organization slug a client_id names, or nil for one without it
(issued before sc-1463).
The current configuration, with ENDPOINTBLANK_* fallbacks applied.
Merges opts into the stored config.
Max number of concurrent writes DelayedWriter performs per flush tick.
Falls back to the struct default (4) if unset or invalid.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
The organization slug a client_id names, or nil for one without it
(issued before sc-1463).
The same rule as app_portal's Credentials.client_id_slug/1, and the rule
all five SDKs will share: the part before the first . must have the exact shape of an
organization slug, and something must follow the dot. "Contains a ." is
not enough, because app_portal has always accepted a typed client_id, so a
legacy my.client can exist and must keep calling the default intake.
The current configuration, with ENDPOINTBLANK_* fallbacks applied.
A direct ETS read in the calling process — no message is sent to the Agent, so this can neither queue behind a config write nor exit on a call timeout.
Raises if the store is unavailable; see the module doc for why it does not fall back to a default config.
Merges opts into the stored config.
Raises ArgumentError if opts contains any key that is not a setting —
including :__struct__, which is otherwise a "valid" key of the struct but
would corrupt it if put. The check is all-or-nothing: if any key is
unknown, none of opts is applied, including the keys that were fine.
A misspelled or obsolete key (client_secert:, base_uri:) used to be
dropped silently, which left the host running with nil credentials or the
public default base URL and no indication why. This library does not do
silent fallbacks; a bad configure/1 call is a boot-time bug and should
crash the boot.
Also raises ArgumentError if :cache_ttl is given with a value that is
not a non-negative integer — an explicit nil, a negative number, a float
or a string (sc-970). Omit :cache_ttl for the default of 300 seconds;
0 disables the authorization cache. This is all-or-nothing in the same
way. Before sc-970, nil and strings were stored as given and silently
treated as 300 when the cache was first used, a float was used as a
fractional TTL, and a negative number silently disabled the cache.
Both checks run in the calling process, before Agent.update/2 is called
— never inside the function passed to it. update/1 is called from
arbitrary host code (usually EndPointBlank.configure/1 at boot), and an
exception raised inside Agent.update/2's function crashes the Agent
itself, not just the caller. That Agent is this config store's only writer
and the owner of the read path's ETS table (see the moduledoc), so crashing
it over one bad configure/1 call would take the whole store down for
every reader in the host app, not just reject the one bad call.
Serialised through the Agent, and synchronous: once this returns, every
process reading get/0 sees the new value.
Max number of concurrent writes DelayedWriter performs per flush tick.
Falls back to the struct default (4) if unset or invalid.