EndPointBlank (Elixir)
EndPointBlank client for Elixir / Phoenix apps — 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 a private Hex organization (not on the public
hex.pm index). Add the dependency and pass your organization at fetch time:
def deps do
[
{:end_point_blank_elixir, "~> 0.3", organization: "your-hex-org"}
]
end
Then authenticate the Hex CLI against your org once per machine/CI runner
(mix hex.organization auth your-hex-org) before mix deps.get.
If you don't have Hex org access yet, depend on the git repo directly:
def deps do
[
{:end_point_blank_elixir, git: "https://github.com/EndPointBlank/end_point_blank_elixir.git", tag: "v0.3.1"}
]
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 are held in a singleton EndPointBlank.Config agent (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).
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 |
| Reserved for future writer pooling; not currently read by any writer | :worker_count | — (configure/1 only) | 4 |
Authorization-cache TTL in seconds (EndPointBlank.AuthCache) | :cache_ttl | — (configure/1 only) | 300 |
| Ordered list of masking rule maps (see Data masking) | :masking_rules | — (configure/1 only) | [] |
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,
version_finder: fn conn -> Plug.Conn.get_req_header(conn, "x-api-version") |> List.first() end
)
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. - Sends
Authorization: Bearer <token>when a cached access token exists for the target host (EndPointBlank.AccessTokens), otherwiseBasic <client_id:client_secret>(EndPointBlank.Authorization). - Caches successful authorizations for up to
:cache_ttlseconds (EndPointBlank.AuthCache), keyed on the caller's own auth header, path, HTTP method, andapp_name— repeat calls skip the network round trip. - On a
401from a Bearer-authenticated call, evicts the stale token and retries once with a fresh token (or Basic auth as a last resort). - Stores the resulting
source_application_environment_idinEndPointBlank.RequestStorefor the rest of the request lifecycle (it's attached to request/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.
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 every 100 ms, per endpoint key. 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.
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"], state: "Current"
version_of :index, ["v0"], state: "Deprecated"
def index(conn, _params), do: ...
end
EndpointRegistrar.register/1 introspects router.__routes__/0, merges in
any version_of metadata, and POSTs the endpoint list (path, HTTP method,
and %{state => [versions]}) to your base_url.
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
lib/end_point_blank/auth_cache.ex # ETS-backed authorization result cache
lib/end_point_blank/access_tokens.ex # per-hostname access-token cache
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) — this package is published only to a private Hex organization.