-
Notifications
You must be signed in to change notification settings - Fork 54
Add Elixir SDK to documentation #473
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| --- | ||
| imageDescription: "Challenge, Credential, and Receipt types for working with the MPP protocol in Elixir" | ||
| --- | ||
|
|
||
| # Core types [Challenge, Credential, and Receipt modules] | ||
|
|
||
| These modules map directly to HTTP headers — `WWW-Authenticate`, `Authorization`, and `Payment-Receipt` — and can be used independently of the `MPP.Plug` middleware. | ||
|
|
||
| ## Challenge | ||
|
|
||
| Create an HMAC-bound challenge and format it as a `WWW-Authenticate` header: | ||
|
|
||
| ```elixir | ||
| alias MPP.{Challenge, Headers} | ||
|
|
||
| challenge = Challenge.create( | ||
| secret_key: "your-hmac-secret", | ||
| realm: "api.example.com", | ||
| method: "stripe", | ||
| intent: "charge", | ||
| request_b64: Base.url_encode64(Jason.encode!(%{amount: "1000", currency: "usd"}), padding: false) | ||
| ) | ||
|
|
||
| header = Headers.format_www_authenticate(challenge) | ||
| # → "Payment id=\"abc...\", realm=\"api.example.com\", ..." | ||
| ``` | ||
|
|
||
| Verify a challenge ID on the retry request: | ||
|
|
||
| ```elixir | ||
| {:ok, true} = Challenge.verify(challenge, "your-hmac-secret") | ||
| ``` | ||
|
|
||
| ## Credential | ||
|
|
||
| Parse an `Authorization: Payment` header into a credential struct: | ||
|
|
||
| ```elixir | ||
| alias MPP.{Credential, Headers} | ||
|
|
||
| {:ok, credential} = Headers.parse_authorization(auth_header) | ||
|
|
||
| # Access the echoed challenge fields | ||
| credential.challenge_id | ||
| credential.method | ||
| credential.intent | ||
|
|
||
| # Access the payment payload | ||
| credential.payload | ||
| ``` | ||
|
|
||
| ## Receipt | ||
|
|
||
| Build a receipt and format it as a `Payment-Receipt` header: | ||
|
|
||
| ```elixir | ||
| alias MPP.{Receipt, Headers} | ||
|
|
||
| receipt = %Receipt{ | ||
| status: "success", | ||
| challenge_id: challenge.id, | ||
| method: "stripe", | ||
| reference: "pi_abc123" | ||
| } | ||
|
|
||
| header = Headers.format_receipt(receipt) | ||
| # → base64url-encoded JSON | ||
| ``` | ||
|
|
||
| ## Headers | ||
|
|
||
| The `MPP.Headers` module handles all header parsing and formatting: | ||
|
|
||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `format_www_authenticate/1` | Serialize a Challenge to a `WWW-Authenticate` header value | | ||
| | `format_receipt/1` | Serialize a Receipt to a `Payment-Receipt` header value | | ||
| | `parse_authorization/1` | Parse an `Authorization` header into a Credential | | ||
|
|
||
| ## Type reference | ||
|
|
||
| | Module | Description | | ||
| |--------|-------------| | ||
| | `MPP.Challenge` | Server challenge with HMAC-SHA256 ID binding | | ||
| | `MPP.Credential` | Client credential parsed from `Authorization` header | | ||
| | `MPP.Receipt` | Server receipt for `Payment-Receipt` header | | ||
| | `MPP.Headers` | Header parsing and formatting utilities | | ||
| | `MPP.Intents.Charge` | Charge intent request schema (amount, currency, recipient) | | ||
| | `MPP.Errors` | RFC 9457 Problem Detail error types | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| --- | ||
| title: "Elixir SDK for MPP" | ||
| imageDescription: "The mpp Elixir library for adding payment-gated endpoints to Phoenix and Plug applications" | ||
| --- | ||
|
|
||
| import * as SdkBadge from '../../../components/SdkBadge' | ||
|
|
||
| # Elixir SDK [Plug middleware for Phoenix and Plug apps] | ||
|
|
||
| ## Overview | ||
|
|
||
| The `mpp` Elixir library provides Plug middleware that adds pay-per-call billing to any Phoenix or Plug application. Mount the plug on a route, set a price, and the middleware handles the full 402 challenge/credential/receipt flow. | ||
|
|
||
| <div className="flex gap-2"> | ||
| <SdkBadge.GitHub repo="ZenHive/mpp" /> | ||
| <SdkBadge.Maintainer name="ZenHive" href="https://github.com/ZenHive" /> | ||
| </div> | ||
|
|
||
| ## Install | ||
|
|
||
| Add `mpp` to your dependencies in `mix.exs`: | ||
|
|
||
| ```elixir [mix.exs] | ||
| defp deps do | ||
| [ | ||
| {:mpp, "~> 0.1"} | ||
| ] | ||
| end | ||
| ``` | ||
|
|
||
| Then fetch dependencies: | ||
|
|
||
| ```bash [install.sh] | ||
| $ mix deps.get | ||
| ``` | ||
|
|
||
| ## Requirements | ||
|
|
||
| - Elixir 1.17+ | ||
| - A Plug-compatible application (Phoenix, Bandit, Cowboy) | ||
|
|
||
| ## Quick start | ||
|
|
||
| ### Server | ||
|
|
||
| Mount `MPP.Plug` in a Phoenix router to gate endpoints behind payment: | ||
|
|
||
| ```elixir [router.ex] | ||
| defmodule MyAppWeb.Router do | ||
| use MyAppWeb, :router | ||
|
|
||
| pipeline :paid do | ||
| plug MPP.Plug, | ||
| secret_key: "your-hmac-secret", | ||
| realm: "api.example.com", | ||
| method: MPP.Methods.Stripe, | ||
| method_config: %{stripe_secret_key: System.get_env("STRIPE_SECRET_KEY")}, | ||
| amount: "1000", | ||
| currency: "usd" | ||
| end | ||
|
|
||
| scope "/api", MyAppWeb do | ||
| pipe_through [:api, :paid] | ||
| get "/data", DataController, :show | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| The middleware handles the full flow automatically: | ||
|
|
||
| 1. Request without `Authorization: Payment` header returns `402` with a `WWW-Authenticate` challenge | ||
| 2. Client pays off-band (e.g. via Stripe), retries with `Authorization: Payment <credential>` | ||
| 3. Valid credential passes through with a `Payment-Receipt` header attached | ||
| 4. Invalid credential returns `402` with a fresh challenge and an RFC 9457 error body | ||
|
|
||
| ### Per-route pricing | ||
|
|
||
| Each route can have its own price — just mount the plug with different options: | ||
|
|
||
| ```elixir [router.ex] | ||
| scope "/api", MyAppWeb do | ||
| pipe_through :api | ||
|
|
||
| scope "/basic" do | ||
| plug MPP.Plug, amount: "100", currency: "usd", | ||
| secret_key: secret, realm: realm, method: MPP.Methods.Stripe | ||
| get "/data", DataController, :show | ||
| end | ||
|
|
||
| scope "/premium" do | ||
| plug MPP.Plug, amount: "5000", currency: "usd", | ||
| secret_key: secret, realm: realm, method: MPP.Methods.Stripe | ||
| get "/report", ReportController, :show | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| ## Design | ||
|
|
||
| - **Stateless**: Challenge verification uses HMAC-SHA256 binding — no database or challenge store needed | ||
| - **Explicit configuration**: No `Application.get_env` or ENV fallbacks — pass credentials directly via plug opts | ||
| - **Pluggable methods**: Implement the `MPP.Method` behaviour to add new payment methods beyond Stripe | ||
|
|
||
| ## Next steps | ||
|
|
||
| - [Core types](/sdk/elixir/core): Challenge, Credential, and Receipt modules | ||
| - [Server](/sdk/elixir/server): Plug middleware configuration and usage |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| --- | ||
| imageDescription: "Protect Phoenix and Plug endpoints with payment requirements using MPP middleware" | ||
| --- | ||
|
|
||
| # Server [Plug middleware for payment-gated endpoints] | ||
|
|
||
| Mount `MPP.Plug` in any Phoenix or Plug router to protect endpoints with payment. The plug handles the full 402 challenge/credential/receipt flow. | ||
|
|
||
| ## Quick start | ||
|
|
||
| ```elixir | ||
| plug MPP.Plug, | ||
| secret_key: "your-hmac-secret", | ||
| realm: "api.example.com", | ||
| method: MPP.Methods.Stripe, | ||
| method_config: %{stripe_secret_key: "sk_..."}, | ||
| amount: "1000", | ||
| currency: "usd" | ||
| ``` | ||
|
|
||
| ## Options | ||
|
|
||
| ### secret_key (required) | ||
|
|
||
| - **Type:** `String.t()` | ||
|
|
||
| HMAC-SHA256 key for stateless challenge ID binding. The server recomputes the HMAC on verification — no challenge store needed. | ||
|
|
||
| ### realm (required) | ||
|
|
||
| - **Type:** `String.t()` | ||
|
|
||
| Server protection space, included in `WWW-Authenticate` headers. Typically your API domain. | ||
|
|
||
| ### method (required) | ||
|
|
||
| - **Type:** module implementing `MPP.Method` | ||
|
|
||
| Payment method module for verifying credentials. Built-in: `MPP.Methods.Stripe`. | ||
|
|
||
| ### amount (required) | ||
|
|
||
| - **Type:** `String.t()` | ||
|
|
||
| Price in base units (e.g. `"1000"` for $10.00 USD with Stripe). | ||
|
|
||
| ### currency (required) | ||
|
|
||
| - **Type:** `String.t()` | ||
|
|
||
| Currency code, normalized to lowercase (e.g. `"usd"`). | ||
|
|
||
| ### method_config (optional) | ||
|
|
||
| - **Type:** `map()` | ||
|
|
||
| Server-only configuration passed to `MPP.Method.verify/2` via `charge.method_details`. Use this for secrets like API keys that the payment method needs but clients must not see. Never serialized into challenges. | ||
|
|
||
| ```elixir | ||
| method_config: %{stripe_secret_key: System.get_env("STRIPE_SECRET_KEY")} | ||
| ``` | ||
|
|
||
| ### recipient (optional) | ||
|
|
||
| - **Type:** `String.t()` | ||
|
|
||
| Payment recipient identifier included in the charge intent. | ||
|
|
||
| ### description (optional) | ||
|
|
||
| - **Type:** `String.t()` | ||
|
|
||
| Human-readable description attached to the challenge. | ||
|
|
||
| ### expires_in (optional) | ||
|
|
||
| - **Type:** `integer()` | ||
|
|
||
| Challenge TTL in seconds. Defaults to 300 (5 minutes). | ||
|
|
||
| ### opaque (optional) | ||
|
|
||
| - **Type:** `String.t()` | ||
|
|
||
| Base64url-encoded server correlation data included in the challenge. | ||
|
|
||
| ## Phoenix router example | ||
|
|
||
| ```elixir | ||
| defmodule MyAppWeb.Router do | ||
| use MyAppWeb, :router | ||
|
|
||
| pipeline :paid do | ||
| plug MPP.Plug, | ||
| secret_key: Application.compile_env!(:my_app, :mpp_secret_key), | ||
| realm: "api.example.com", | ||
| method: MPP.Methods.Stripe, | ||
| method_config: %{ | ||
| stripe_secret_key: Application.compile_env!(:my_app, :stripe_secret_key) | ||
| }, | ||
| amount: "1000", | ||
| currency: "usd" | ||
| end | ||
|
|
||
| scope "/api", MyAppWeb do | ||
| pipe_through [:api, :paid] | ||
| get "/data", DataController, :show | ||
| get "/report", ReportController, :show | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| ## Accessing the receipt | ||
|
|
||
| After successful payment verification, the receipt is available in `conn.assigns`: | ||
|
|
||
| ```elixir | ||
| def show(conn, _params) do | ||
| receipt = conn.assigns[:mpp_receipt] | ||
| json(conn, %{data: "paid content", reference: receipt.reference}) | ||
| end | ||
| ``` | ||
|
|
||
| ## Custom payment methods | ||
|
|
||
| Implement the `MPP.Method` behaviour to add new payment methods: | ||
|
|
||
| ```elixir | ||
| defmodule MyApp.Payments.CustomMethod do | ||
| @behaviour MPP.Method | ||
|
|
||
| @impl true | ||
| def verify(credential, charge) do | ||
| # Verify the payment and return a receipt or error | ||
| {:ok, %MPP.Receipt{status: "success", reference: "ref_123", ...}} | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| Then use it in your plug configuration: | ||
|
|
||
| ```elixir | ||
| plug MPP.Plug, | ||
| method: MyApp.Payments.CustomMethod, | ||
| # ... | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,17 @@ | ||
| --- | ||
| title: "SDKs and client libraries" | ||
| description: "Official MPP SDK implementations in TypeScript, Python, and Rust. Libraries for building MPP clients and servers with full protocol support." | ||
| imageDescription: "Official MPP SDK implementations in TypeScript, Python, and Rust for clients, servers, and agents" | ||
| description: "MPP SDK implementations in TypeScript, Python, Rust, and Elixir. Libraries for building MPP clients and servers with full protocol support." | ||
| imageDescription: "MPP SDK implementations in TypeScript, Python, Rust, and Elixir for clients, servers, and agents" | ||
| --- | ||
|
|
||
| import { Cards } from 'vocs' | ||
| import { TypeScriptSdkCard, PythonSdkCard, RustSdkCard, WalletCliCard } from '../../components/cards' | ||
| import { TypeScriptSdkCard, PythonSdkCard, RustSdkCard, ElixirSdkCard, WalletCliCard } from '../../components/cards' | ||
|
|
||
| # SDKs [Official implementations in multiple languages] | ||
|
|
||
|
||
| <Cards> | ||
| <TypeScriptSdkCard /> | ||
| <PythonSdkCard /> | ||
| <RustSdkCard /> | ||
| <ElixirSdkCard /> | ||
| </Cards> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WalletCliCardis imported here but never used in this MDX file. Please remove it from the import list to avoid unused-import lint/TS checks failing (or add the card to the<Cards>section if it was intended to be shown).