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.

SettingConfig keyEnv varDefault
API client ID:client_idENDPOINTBLANK_CLIENT_IDnil
API client secret:client_secretENDPOINTBLANK_CLIENT_SECRETnil
Authorization/update API base URL:base_urlENDPOINTBLANK_BASE_URL"https://in.endpointblank.com"
Request/response/log/error ingestion base URL:log_base_urlENDPOINTBLANK_LOG_BASE_URL"https://log.endpointblank.com"
Application identifier sent with every payload:app_nameENDPOINTBLANK_APP_NAMEnil
Deployment environment (e.g. "production"):environmentENDPOINTBLANK_ENVnil
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:

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:

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

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.