EndPointBlank (Elixir)
Elixir and Phoenix SDK for EndPointBlank: authorize service-to-service API calls, report endpoint versions, and see which clients still call deprecated API versions. It covers endpoint tracking and authorization, request/response/error/log reporting, and client-side data masking, all reporting back to the EndPointBlank API.
Installation
This package is published to the public hex.pm repository:
def deps do
[
{:end_point_blank_elixir, "~> 0.7.0"}
]
end
Pin to the patch level (~> 0.7.0, not ~> 0.7): before 1.0, breaking
changes ship in minor releases, so ~> 0.7 would accept a future 0.8.0.
Or depend on a release tag of the git repo directly:
def deps do
[
{:end_point_blank_elixir, git: "https://github.com/EndPointBlank/end_point_blank_elixir.git", tag: "v0.7.0"}
]
end
The library starts its own supervision tree (EndPointBlank.Application) as
soon as it's listed as a dependency — no extra child spec to add to your app.
Quick start
Configure credentials (typically in application.ex's start/2, before your
endpoint starts) and wire in the two plugs:
EndPointBlank.configure(
client_id: "my-client-id",
client_secret: "my-client-secret",
app_name: "my-app",
environment: "production"
)
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
# ... other plugs ...
plug EndPointBlank.Plug.ReportInteraction
plug MyAppWeb.Router
end
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :api do
plug :accepts, ["json"]
plug EndPointBlank.Plug.Authorized
end
end
With just this, every request/response pair is reported to EndPointBlank, and every request is authorized against your configured application before it reaches your controllers.
Configuration
All settings live in a singleton EndPointBlank.Config store (started by
EndPointBlank.Application), set via EndPointBlank.configure/1, and read
back via EndPointBlank.Config.get/0. Six of them also fall back to
ENDPOINTBLANK_* environment variables so you can run without any
Elixir-side configuration at all (e.g. purely env-driven deployments).
Writes go through an Agent, which serialises them. Reads do not: Config.get/0
is a lock-free ETS lookup performed in the calling process, because it sits in
the hot path of every inbound request and every outbound write. Nothing your
app does can queue behind a config write, and a busy config process cannot
take a request down with it. If the store is unavailable — the application is
not started, or the config process is down — Config.get/0 raises. It
does not fall back to a blank config: that would mean authorizing against
nil credentials and shipping telemetry to the default base URL because a
process happened to be down.
Precedence (per setting, resolved fresh on every Config.get/0 call —
the env var is never cached): explicit configure/1 value > ENDPOINTBLANK_*
env var > built-in default.
| Setting | Config key | Env var | Default |
|---|---|---|---|
| API client ID | :client_id |
ENDPOINTBLANK_CLIENT_ID |
nil |
| API client secret | :client_secret |
ENDPOINTBLANK_CLIENT_SECRET |
nil |
| Authorization/update API base URL | :base_url |
ENDPOINTBLANK_BASE_URL |
"https://in.endpointblank.com" |
| Request/response/log/error ingestion base URL | :log_base_url |
ENDPOINTBLANK_LOG_BASE_URL |
"https://log.endpointblank.com" |
| Application identifier sent with every payload | :app_name |
ENDPOINTBLANK_APP_NAME |
nil |
Deployment environment (e.g. "production") |
:environment |
ENDPOINTBLANK_ENV |
nil |
App version string (e.g. a git SHA), sent as app_version on endpoint registration |
:application_version |
— (configure/1 only) |
nil |
Custom 1-arity API-version detector, fn conn -> version end |
:version_finder |
— (configure/1 only) |
nil |
Access-token TTL in seconds (sent to GenerateAccessToken) |
:token_ttl |
— (configure/1 only) |
nil |
Post-rule masking hook, fn payload, record_type -> payload end |
:mask_hook |
— (configure/1 only) |
nil |
Write mode: :direct (synchronous HTTP per payload) or :delayed (batched background queue) |
:log_mode |
— (configure/1 only) |
:direct |
Max concurrent writes EndPointBlank.Writers.DelayedWriter performs per flush |
:worker_count |
— (configure/1 only) |
4 |
Authorization-cache TTL in seconds (EndPointBlank.AuthCache); 0 disables the cache. See :cache_ttl values |
:cache_ttl |
— (configure/1 only) |
300 |
Whether the per-request scheme/host/port report honors x-forwarded-proto/-host/-port (see Reported base URL) |
:trust_proxy_headers |
— (configure/1 only) |
true |
| Ordered list of masking rule maps (see Data masking) | :masking_rules |
— (configure/1 only) |
[] |
Derive the intake hostname from a slug-prefixed client_id when no base URL is set (see Intake hostname from client_id) |
:derive_base_url_from_client_id |
— (configure/1 only) |
false |
:cache_ttl values
:cache_ttl follows one rule, the same in the Elixir, JS, Java, Python and
Rails SDKs:
| You pass | Result |
|---|---|
nothing (omit :cache_ttl) |
the default, 300 seconds |
a positive integer, e.g. cache_ttl: 60 |
cache authorizations for that many seconds |
cache_ttl: 0 |
caching disabled |
cache_ttl: nil |
ArgumentError from configure/1 |
a negative integer, e.g. cache_ttl: -1 |
ArgumentError from configure/1 |
anything else that is not an integer: a float (3.5, 300.0), a string ("300") |
ArgumentError from configure/1 |
The error is raised by the configure/1 call itself, so a bad value fails at
boot rather than when the first request is authorized, and nothing else in
that configure/1 call is applied. To get the default, leave :cache_ttl out;
nil does not mean "use the default". A value read from an environment
variable arrives as a string, so convert it first
(String.to_integer(System.fetch_env!("MY_CACHE_TTL"))).
Configure example (all settings)
EndPointBlank.configure(
base_url: "https://in.endpointblank.com",
log_base_url: "https://log.endpointblank.com",
client_id: "my-client-id",
client_secret: "my-client-secret",
app_name: "my-app",
environment: "production",
application_version: System.get_env("GIT_SHA"),
log_mode: :delayed,
token_ttl: 3600,
cache_ttl: 300,
trust_proxy_headers: true,
version_finder: fn conn -> Plug.Conn.get_req_header(conn, "x-api-version") |> List.first() end
)
Reported base URL
Every request payload carries the base URL the caller used, as three separate
fields — scheme, host and port. A field that cannot be resolved is omitted
rather than sent as null. EndPointBlank uses these to fill in an application
environment's base URL for you, instead of asking someone to type it.
By default the library honors x-forwarded-proto, x-forwarded-host and
x-forwarded-port, reading the last comma-separated hop, straight off
conn.req_headers. Plug has no notion of a trusted proxy unless your
application installs Plug.RewriteOn itself, which is why this client could not
previously see through a load balancer at all. It resolves the headers the same
way the Ruby, JS, Python and Java clients do, so all five answer identically for
the same request.
Turn this off if your application is reachable directly, with no proxy in front of it — or if you would simply rather report nothing than report something a caller could influence:
EndPointBlank.configure(trust_proxy_headers: false)
With it off, the x-forwarded-* headers are ignored entirely and scheme,
host and port come from the conn and the host header only.
It defaults to true because the alternative is worse for almost everyone. Most
production deployments sit behind an ALB, nginx, Caddy or an Ingress, and a
client that ignored the forwarded headers there would not report nothing — it
would confidently report an internal hostname on an internal port. host is
caller-controlled either way (conn.host comes from the host header), and
none of these three values is ever used as an identity or authorization key, so
the worst case is a wrong suggestion that an admin has to approve.
Intake hostname from client_id
Each organization's intake will answer at its own hostname,
https://<slug>.in.endpointblank.com, and every new client_id starts with
that slug and a dot (acima-x7k2mq.ijXI+MVwmrC5xH/9ZuGiQlAbAyobTqMa). With
derive_base_url_from_client_id: true, the SDK picks its intake in this
order:
:base_url, or elseENDPOINTBLANK_BASE_URL, if either is set;- else, if the
client_idcarries a slug prefix,https://<slug>.in.endpointblank.com; - else
https://in.endpointblank.com.
A client_id carries a slug prefix only when the part before its first .
has the exact shape of an organization slug and something follows the dot
(EndPointBlank.Config.client_id_slug/1). A credential issued before slugs,
including one with a . in it such as my.client, keeps calling
https://in.endpointblank.com.
This is off by default, and turns on by default in a later release, once
DNS and TLS for *.in.endpointblank.com are live. Until then those
hostnames do not resolve in production, so leave it off unless EndPointBlank
has told you otherwise. With it off, the base URL is :base_url, else
ENDPOINTBLANK_BASE_URL, else https://in.endpointblank.com, whatever the
client_id.
The logs hostname is not derived: :log_base_url, else
ENDPOINTBLANK_LOG_BASE_URL, else https://log.endpointblank.com, as before.
Every call to intake also sends x-epb-sdk: elixir/<version>, so
EndPointBlank can tell which SDK versions use a credential before it moves an
organization to another intake. The minimum Elixir version for a move is the
release that turns derive_base_url_from_client_id on by default, not
this one: with the option at its default here, the SDK keeps calling
https://in.endpointblank.com after its organization has moved.
12-factor / env-var example
Only these six settings have an env-var fallback; everything else must be set
via EndPointBlank.configure/1 (there's no :log_mode or :masking_rules
env var, for example):
export ENDPOINTBLANK_CLIENT_ID="my-client-id"
export ENDPOINTBLANK_CLIENT_SECRET="my-client-secret"
export ENDPOINTBLANK_APP_NAME="my-app"
export ENDPOINTBLANK_ENV="production"
export ENDPOINTBLANK_BASE_URL="https://in.endpointblank.com"
export ENDPOINTBLANK_LOG_BASE_URL="https://log.endpointblank.com"
With just the env vars set, you can skip EndPointBlank.configure/1 entirely
(or call it with only the settings that don't have an env fallback, like
log_mode:).
Usage
Authorization
EndPointBlank.Plug.Authorized calls the EndPointBlank /api/authorize
endpoint for the current request and halts with a 401 (authorization
denied) or 503 (service unavailable) JSON response on failure. It can be
used as a controller plug or in a router pipeline:
defmodule MyAppWeb.BooksController do
use Phoenix.Controller
plug EndPointBlank.Plug.Authorized
...
end
pipeline :api do
plug :accepts, ["json"]
plug EndPointBlank.Plug.Authorized
end
Under the hood it:
-
Resolves the route pattern via
EndPointBlank.Phoenix.RoutePatternFinder(falls back toconn.request_pathif no Phoenix router is present) and the API version viaEndPointBlank.VersionFinder. -
Authenticates to intake with
Authorization: Basic <client_id:client_secret>(EndPointBlank.Authorization.intake_header/0). This call never presents a Bearer token — intake already holds this service's credential, so minting one to present it back would be a hop that buys nothing, and with no Bearer there is nothing that can go stale for a401to retry. Without bothclient_idandclient_secretnothing is sent: the plug logs why and answers 503, failing closed. -
Caches successful authorizations for up to
:cache_ttlseconds (EndPointBlank.AuthCache), keyed on the caller's own auth header, path, HTTP method,app_name, and API version — repeat calls skip the network round trip.:cache_ttlis re-checked on every read, not just applied to entries written after it changes: lowering it shortens the remaining life of everything already cached (an entry never outlives whichever is smaller, the TTL it was written with or the TTL currently configured), and raising it never extends an entry past what it was written with. Setting:cache_ttlto0disables the cache outright: every lookup misses, and an authorize call made while disabled deletes every entry already stored, not merely the one it looked up — so raising:cache_ttlback up afterwards cannot resurrect anything that was cached before that call. This is the SDK's answer to "force-flush a revoked grant", but note two things before relying on it: the trigger, and the scope.The trigger: the flush happens on the next authorize call (or a direct
EndPointBlank.AuthCache.get/1/put/2) made while:cache_ttlis0, not at the momentconfigure/1sets it. Disabling and re-enabling with no authorize call in between flushes nothing.The scope:
:cache_ttl, "disabled", and the cache table are each per BEAM node — none of them is shared or propagated across a cluster.:cache_ttlcomes from this node's own configuration, set only viaconfigure/1(there is noENDPOINTBLANK_CACHE_TTLor other env var for it — see the settings table above), which only ever affects the node it runs on, and the cache is an ETS table, which ETS never distributes. So runningconfigure(cache_ttl: 0)on one node (say, node A) does not disable node B or node C at all: their:cache_ttlis whatever it already was, they are not "disabled" by any definition this module has, and a revoked grant already cached on either of them keeps answering for up to its own TTL — nothing here makes B or C "find out" that A was disabled, no matter how much traffic reaches them. Flushing every node requires setting:cache_ttlto0on each node individually and getting an authorize call (or a directget/1/put/2) to land on each of them while it is disabled. CallEndPointBlank.AuthCache.clear/0directly, on every node, when a flush is the goal and that is not something you can rely on. -
Stores the
source_application_environment_idfrom the response'sdatalist inEndPointBlank.RequestStorefor the rest of the request lifecycle (it's attached to response/log/error payloads).
EndPointBlank.UnauthorizedError is available for your own code to raise on
authorization failures. EndPointBlank.Plug.ReportInteraction (below)
specifically re-raises it without sending it to the error-reporting
endpoint, since the authorization flow already reports the denial itself.
Calling another EndPointBlank-protected service
EndPointBlank.Authorization.header/1 is the public building block for your
own outbound calls to other services protected by EndPointBlank (providers).
Pass the URL you are about to call, not a hostname — intake normalizes the
base URL and matches it against registered base URLs by longest path prefix,
so you do not need to know how the target registered itself. Its userinfo,
query and fragment are removed before the token request, the scheme and host
are lowercased, and a default port (:443 for https, :80 for http) or an
empty one is dropped; userinfo, query and fragment are never sent to intake,
logged, or kept on the error. A URL that does not parse, has no host, has a
scheme other than http or https, or has a port that is not a number from
1 to 65535 is refused with {:error, :invalid_base_url} without asking
intake. header!/1 raises EndPointBlank.TokenUnavailableError
for it (reason :invalid_base_url, or :missing_base_url for a missing URL);
the Ruby SDK raises ArgumentError for both:
url = "https://api.example.com/orders"
case EndPointBlank.Authorization.header(url) do
{:ok, auth} ->
# "Bearer <token>", minting one via EndPointBlank.AccessTokens if none is
# held yet.
Req.post(url, json: order, headers: [{"authorization", auth}])
{:error, reason} ->
# No token could be obtained. Do NOT make the call, and do not send
# Basic credentials yourself.
{:error, EndPointBlank.TokenUnavailableError.message(url, reason)}
end
# Or, raising EndPointBlank.TokenUnavailableError (or
# EndPointBlank.ConfigurationError, below) instead:
auth = EndPointBlank.Authorization.header!(url)
If client_id or client_secret is not configured (nil or empty), nothing
is sent: header/1 answers {:error, :missing_credentials} and header!/1
raises EndPointBlank.ConfigurationError. Before, the token request went out
with an empty credential, intake answered 401, and the error said to re-issue
a credential that had simply never been set. The SDK's own calls to intake
refuse the same way, without sending anything: the authorize plug answers 503,
and the endpoint update at boot and the writers log "EndPointBlank is missing
client_id and client_secret: ..." and return. None of them raises into your
application; only header!/1 (and Authorization.intake_header!/0) raise.
Your client_id/client_secret is never sent to a provider. When a token
cannot be minted — intake is down or times out, answers 5xx, or rejects the
credential with 401 — header/1 answers {:error, reason} and header!/1
raises EndPointBlank.TokenUnavailableError, whose message says what failed
and why. Neither falls back to HTTP Basic. Until sc-1469 header/1 did, which
handed the credential to the provider. A missing or empty URL is
{:error, :missing_base_url}; there is no no-argument header/0.
reason is one of the failures below, plus :missing_base_url,
:invalid_base_url, :missing_credentials, :token_cache_unavailable (the
token cache did not answer in time) and {:unexpected, reason}: the mint
raised, threw, or the HTTP client answered something that is not a transport
failure — a bug or a bad setting, not intake being out of reach. Only a real
failure to reach intake (a timeout, a refused connection) is
{:transport_error, reason}. EndPointBlank.TokenUnavailableError carries
the reason as :reason alongside :base_url (the stripped URL, or nil when
there was none that parsed), :status, the HTTP status intake answered with
(401 for :credential_rejected, the status in {:request_rejected, status}
or {:server_error, status}, otherwise nil), and :unexpected, true for
{:unexpected, _}. The message is built from fixed
phrases, the same in every EndPointBlank SDK, and never repeats intake's
response body, a transport error or an exception: for example "intake
rejected this application's client credential (HTTP 401); retrying cannot
help -- re-issue the credential", or "intake could not be reached (timeout,
connection refused or retries exhausted); this may be transient". A mint that
raised reads "the token request failed unexpectedly". The raw term stays on
:reason.
EndPointBlank.AccessTokens caches one token per application environment,
keyed on the canonical base URL intake resolves the request to — not on the
URL you passed — so a service that calls several targets holds a token for
each. Every call this SDK makes to its own intake (authorize, token minting,
endpoint updates and the writers) uses
EndPointBlank.Authorization.intake_header/0 instead, which answers
{:ok, "Basic ..."} or {:error, :missing_credentials}; intake already holds
the credential. Never use it, or basic_header/0, for a call to a provider.
Finding out why a token could not be minted
header/1 answers the reason for the mint it just attempted, and
AccessTokens.token_result/1 does the same for the bare token
(AccessTokens.token/1 answers nil for every failure). Not every failure is
an outage: intake answers 401 when
the API credential itself has been rejected, and that is permanent until
someone re-issues it. AccessTokens.last_failure/1 reports the last failure
for a URL so a caller can tell them apart and alarm on the one that will not
fix itself:
case EndPointBlank.AccessTokens.last_failure("https://api.example.com/orders") do
nil -> :ok
# Permanent — retrying changes nothing.
:credential_rejected -> alarm("re-issue the EndPointBlank credential")
{:request_rejected, status} -> alarm("intake refused the request: #{status}")
# Transient — worth trying again.
{:server_error, _status} -> :ok
{:transport_error, _reason} -> :ok
end
{:server_error, status} also covers a 2xx the SDK cannot read an access
token out of — an undecodable body, or one carrying no token or no
base_url. The status is the real one intake sent; why a 2xx was unusable
is in the log line rather than in the return value.
Classification is on the HTTP status alone; the body never overrides a status
that was actually received. A 401 whose body is not JSON — which is what a
proxy or gateway in front of intake answers — is still :credential_rejected.
{:transport_error, reason} means no HTTP status was obtained at all.
last_failure/1 records only what intake's answer (or its absence) says:
:missing_credentials and {:unexpected, _} are answered by header/1 and
token_result/1 but never recorded, and do not drop a held token.
A successful mint clears the record, and only the 64 most recently failed URLs are held — ask about a URL you just called and it will be there.
EndPointBlank.Commands.GenerateAccessToken.generate_result/1
is the same distinction one layer down, for callers that mint directly:
{:ok, payload} or {:error, reason} with the same reasons.
generate/1 still answers payload-or-nil.
Request/response/log reporting
EndPointBlank.Plug.ReportInteraction reports every request/response pair
and any unhandled exception. Place it early in your endpoint, before routing:
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
plug EndPointBlank.Plug.ReportInteraction
plug MyAppWeb.Router
end
It generates a per-request UUID (EndPointBlank.RequestStore), writes the
request immediately via EndPointBlank.Writers.RequestWriter, registers a
before_send callback that writes the response via
EndPointBlank.Writers.ResponseWriter, and — for any exception that
propagates up (other than EndPointBlank.UnauthorizedError) — reports it via
EndPointBlank.Writers.ExceptionWriter before re-raising, so your normal
error handling / Plug.ErrorHandler still runs.
Request and response bodies are JSON-encoded and truncated to 1024 bytes before being sent.
For structured application logs, call EndPointBlank.Writers.LogWriter
directly from anywhere in your app (it picks up the current request's UUID
from RequestStore automatically, if any):
EndPointBlank.Writers.LogWriter.info("Fetching books list")
EndPointBlank.Writers.LogWriter.warn("Slow query", %{duration_ms: 820})
EndPointBlank.Writers.LogWriter.error("Payment provider timeout", %{provider: "stripe"})
EndPointBlank.Writers.LogWriter.fatal("Out of retries", %{job_id: job.id})
All four writers (RequestWriter, ResponseWriter, ExceptionWriter,
LogWriter) dispatch through EndPointBlank.Writers, honoring :log_mode:
-
:direct(default) — sends synchronously viaEndPointBlank.Writers.DirectWriter. -
:delayed— enqueues ontoEndPointBlank.Writers.DelayedWriter, aGenServerthat batches up to 4 payloads per flush, per endpoint key, and flushes once a second. Each key's queue is capped at 1,000 payloads; under a sustained intake outage the oldest payloads for that key are dropped (and a warning logged) rather than growing memory unbounded.A batch that fails costs that batch and nothing more. Every way a flush can fail — a raise anywhere under
DirectWriter.write/2, or aGenServer.calltimeout that exits its caller — is caught, logged aterrorlevel once per failing flush (with a running count of consecutive failures), and backed off: the flush interval doubles from 1 s up to a 30 s ceiling while failures continue, and resets on the first clean flush. Telemetry delivery can degrade; it cannot take your application's supervision tree with it.
All outbound HTTP goes through EndPointBlank.Http.post/3, which retries up
to 3 times (200 ms apart) on network error, with a 3 s connect timeout and a
5 s receive timeout per attempt, so a hung intake can never block the caller
indefinitely.
Endpoint registration (Phoenix)
Register your Phoenix router's endpoints (and any per-action version metadata) with EndPointBlank at application startup:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
EndPointBlank.configure(client_id: "...", client_secret: "...", app_name: "my-app")
EndPointBlank.Phoenix.EndpointRegistrar.register(MyAppWeb.Router)
children = [MyAppWeb.Endpoint]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
end
Declare per-action version metadata on a controller with
EndPointBlank.Phoenix.Versioned:
defmodule MyAppWeb.BooksController do
use Phoenix.Controller
use EndPointBlank.Phoenix.Versioned
version_of :index, ["v1", "v2"]
version_of :index, ["v0"]
def index(conn, _params), do: ...
end
version_of/2 takes an action and the list of versions it serves; repeated
calls for the same action merge, deduplicated, in declaration order. Lifecycle
state (Current, Deprecated, ...) is not declared in code — it is managed in
the EndPointBlank portal, so changing it does not require a deploy.
EndpointRegistrar.register/1 introspects router.__routes__/0, merges in
any version_of metadata, and POSTs the versioned endpoints (path, HTTP
method, and the list of versions) to <base_url>/api/application_updates,
alongside the app name, hostname, environment and application version. Routes
whose action has no version_of declaration are not registered.
Data masking
Mask sensitive data before it leaves your app. Configure an ordered list of rules; each rule targets one field and masks by a JSONPath, a regex, or both. (Server-side intake also masks independently, so this is defense in depth.)
EndPointBlank.configure(
masking_rules: [
# Replace any "ssn" field at any depth in the request body.
%{target: "request_body", path: "$..ssn", replacement_value: "***"},
# Keep first/last 4 of a card number in error messages via backreferences.
%{target: "error_message", regex: "(\\d{4})-\\d{4}-\\d{4}-(\\d{4})", replacement_value: "$1-****-****-$2"}
],
# Optional: runs after the rules; last chance to transform the payload.
mask_hook: fn payload, record_type -> payload end
)
Rules are maps with atom keys.
Rule fields
target— exactly one of"request_body","request_headers","path","response_body","error_message".path— an optional JSONPath (supported subset:$,.name,['name'],[n],.*/[*], and..namefor recursive descent). Keys are case-sensitive.regex— an optional regular expression.replacement_value— the replacement string (default"...").
Semantics — path scopes, regex matches within. With only a path, the selected node is replaced
entirely. With only a regex, every matching string is replaced. With both, the regex is applied
only within the path-selected node(s). When a regex is present, replacement_value supports
backreferences: $1, $2, … insert capture groups ($0 the whole match; $$ for a literal $).
Stacktraces and log messages are never masked.
A bad regex or an unparseable path makes that rule a no-op rather than raising — masking never breaks the request it's protecting.
Framework integration
The SDK ships two Plug modules and a Phoenix-only registrar/versioning
pair; nothing requires Phoenix specifically except
EndPointBlank.Phoenix.RoutePatternFinder, EndPointBlank.Phoenix.Versioned,
and EndPointBlank.Phoenix.EndpointRegistrar (any plain-Plug app can still
use Plug.Authorized / Plug.ReportInteraction, just without route-pattern
resolution or endpoint registration).
Full endpoint wiring:
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
# ... session, static, etc. ...
plug EndPointBlank.Plug.ReportInteraction
plug MyAppWeb.Router
end
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :api do
plug :accepts, ["json"]
plug EndPointBlank.Plug.Authorized
end
scope "/api", MyAppWeb do
pipe_through :api
resources "/books", BooksController
end
end
defmodule MyApp.Application do
use Application
def start(_type, _args) do
EndPointBlank.configure(
client_id: System.fetch_env!("ENDPOINTBLANK_CLIENT_ID"),
client_secret: System.fetch_env!("ENDPOINTBLANK_CLIENT_SECRET"),
app_name: "my-app",
environment: Application.get_env(:my_app, :environment)
)
EndPointBlank.Phoenix.EndpointRegistrar.register(MyAppWeb.Router)
children = [MyAppWeb.Endpoint]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
end
Development
mix deps.get
mix test
mix compile --warnings-as-errors
Layout:
lib/end_point_blank.ex # configure/1, version/0
lib/end_point_blank/config.ex # settings + ENDPOINTBLANK_* env fallback
lib/end_point_blank/authorization.ex # Authorization header builder (Bearer-only for providers)
lib/end_point_blank/token_unavailable_error.ex # raised by Authorization.header!/1
lib/end_point_blank/configuration_error.ex # raised by Authorization.header!/1 when a credential is missing
lib/end_point_blank/auth_cache.ex # ETS-backed authorization result cache
lib/end_point_blank/access_tokens.ex # per-application-environment access-token cache, keyed on base URL
lib/end_point_blank/request_store.ex # per-process request-scoped state
lib/end_point_blank/version_finder.ex # API-version detection from a conn
lib/end_point_blank/masking.ex # + masking/json_path.ex
lib/end_point_blank/http.ex # shared HTTP client w/ retries + timeouts
lib/end_point_blank/commands/ # EndpointAuthorize, EndpointUpdate, GenerateAccessToken
lib/end_point_blank/writers/ # Direct/Delayed writers + Request/Response/Log/ExceptionWriter
lib/end_point_blank/plug/ # Authorized, ReportInteraction
lib/end_point_blank/phoenix/ # EndpointRegistrar, Versioned, RoutePatternFinder
test/ # ExUnit test suite
mix docs (via ex_doc, dev-only dependency) builds API reference docs into doc/.
License
Proprietary. See mix.exs (LicenseRef-Proprietary).