From 29f55f395171d45642a74ac4711883f5e35c32a5 Mon Sep 17 00:00:00 2001 From: "E.FU" Date: Thu, 26 Mar 2026 13:02:18 +0800 Subject: [PATCH 1/2] Add Elixir SDK to documentation Add documentation pages for the mpp Elixir SDK (ZenHive/mpp), a community Plug middleware implementation for Phoenix and Plug apps. Closes #456 --- src/components/cards.tsx | 11 +++ src/pages/sdk/elixir/core.mdx | 89 +++++++++++++++++++ src/pages/sdk/elixir/index.mdx | 107 +++++++++++++++++++++++ src/pages/sdk/elixir/server.mdx | 146 ++++++++++++++++++++++++++++++++ src/pages/sdk/index.mdx | 7 +- vocs.config.ts | 10 +++ 6 files changed, 367 insertions(+), 3 deletions(-) create mode 100644 src/pages/sdk/elixir/core.mdx create mode 100644 src/pages/sdk/elixir/index.mdx create mode 100644 src/pages/sdk/elixir/server.mdx diff --git a/src/components/cards.tsx b/src/components/cards.tsx index 0e25a18b..a74af5e6 100644 --- a/src/components/cards.tsx +++ b/src/components/cards.tsx @@ -121,6 +121,17 @@ export function RustSdkCard() { ); } +export function ElixirSdkCard() { + return ( + + ); +} + export function MppxCreateReferenceCard({ to }: { to: string }) { return ( + + + + +## 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 ` +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 diff --git a/src/pages/sdk/elixir/server.mdx b/src/pages/sdk/elixir/server.mdx new file mode 100644 index 00000000..0bbf1978 --- /dev/null +++ b/src/pages/sdk/elixir/server.mdx @@ -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, + # ... +``` diff --git a/src/pages/sdk/index.mdx b/src/pages/sdk/index.mdx index cc9d7fd9..e106092d 100644 --- a/src/pages/sdk/index.mdx +++ b/src/pages/sdk/index.mdx @@ -1,11 +1,11 @@ --- 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] @@ -13,4 +13,5 @@ import { TypeScriptSdkCard, PythonSdkCard, RustSdkCard, WalletCliCard } from '.. + diff --git a/vocs.config.ts b/vocs.config.ts index 40e2218f..b854ca8d 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -64,6 +64,7 @@ export default defineConfig({ { source: "/typescript", destination: "/sdk/typescript" }, { source: "/python", destination: "/sdk/python" }, { source: "/rust", destination: "/sdk/rust" }, + { source: "/elixir", destination: "/sdk/elixir" }, { source: "/reference", destination: "/sdk" }, { source: "/api", destination: "/sdk" }, @@ -728,6 +729,15 @@ export default defineConfig({ { text: "Server", link: "/sdk/rust/server" }, ], }, + { + text: "Elixir", + collapsed: true, + items: [ + { text: "Overview", link: "/sdk/elixir" }, + { text: "Core types", link: "/sdk/elixir/core" }, + { text: "Server", link: "/sdk/elixir/server" }, + ], + }, ], }, { From 15cda74fcf1c83371f8c71ce3a9e1cfc8650bf12 Mon Sep 17 00:00:00 2001 From: "E.FU" Date: Thu, 26 Mar 2026 18:09:03 +0800 Subject: [PATCH 2/2] Address review feedback: community label, hex.pm links, code fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split SDK page into Official and Community sections - Add Community badge to Elixir card - Remove unused WalletCliCard import - Add callout linking to hex.pm and hexdocs for latest version/API docs - Fix method_config keys: atom keys → string keys per actual API - Add missing network_id to Stripe config example - Add method_config to per-route pricing examples --- src/components/cards.tsx | 1 + src/pages/sdk/elixir/index.mdx | 19 +++++++++++++++---- src/pages/sdk/index.mdx | 13 ++++++++++--- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/components/cards.tsx b/src/components/cards.tsx index a74af5e6..d1e1662e 100644 --- a/src/components/cards.tsx +++ b/src/components/cards.tsx @@ -128,6 +128,7 @@ export function ElixirSdkCard() { icon="simple-icons:elixir" title="Elixir" to="/sdk/elixir" + topRight={Community} /> ); } diff --git a/src/pages/sdk/elixir/index.mdx b/src/pages/sdk/elixir/index.mdx index 7b74c88f..9e52949f 100644 --- a/src/pages/sdk/elixir/index.mdx +++ b/src/pages/sdk/elixir/index.mdx @@ -4,6 +4,7 @@ imageDescription: "The mpp Elixir library for adding payment-gated endpoints to --- import * as SdkBadge from '../../../components/SdkBadge' +import { Callout } from 'vocs' # Elixir SDK [Plug middleware for Phoenix and Plug apps] @@ -16,6 +17,10 @@ The `mpp` Elixir library provides Plug middleware that adds pay-per-call billing + + This is a community-maintained SDK. Check [hex.pm/packages/mpp](https://hex.pm/packages/mpp) for the latest version and [hexdocs.pm/mpp](https://hexdocs.pm/mpp) for full API documentation. + + ## Install Add `mpp` to your dependencies in `mix.exs`: @@ -23,7 +28,7 @@ Add `mpp` to your dependencies in `mix.exs`: ```elixir [mix.exs] defp deps do [ - {:mpp, "~> 0.1"} + {:mpp, "~> 0.1"} # check hex.pm for latest version ] end ``` @@ -54,7 +59,10 @@ defmodule MyAppWeb.Router do secret_key: "your-hmac-secret", realm: "api.example.com", method: MPP.Methods.Stripe, - method_config: %{stripe_secret_key: System.get_env("STRIPE_SECRET_KEY")}, + method_config: %{ + "stripe_secret_key" => System.get_env("STRIPE_SECRET_KEY"), + "network_id" => System.get_env("STRIPE_NETWORK_ID") + }, amount: "1000", currency: "usd" end @@ -83,13 +91,15 @@ scope "/api", MyAppWeb do scope "/basic" do plug MPP.Plug, amount: "100", currency: "usd", - secret_key: secret, realm: realm, method: MPP.Methods.Stripe + secret_key: secret, realm: realm, method: MPP.Methods.Stripe, + method_config: stripe_config get "/data", DataController, :show end scope "/premium" do plug MPP.Plug, amount: "5000", currency: "usd", - secret_key: secret, realm: realm, method: MPP.Methods.Stripe + secret_key: secret, realm: realm, method: MPP.Methods.Stripe, + method_config: stripe_config get "/report", ReportController, :show end end @@ -105,3 +115,4 @@ end - [Core types](/sdk/elixir/core): Challenge, Credential, and Receipt modules - [Server](/sdk/elixir/server): Plug middleware configuration and usage +- [Full API docs](https://hexdocs.pm/mpp): Complete module reference on HexDocs diff --git a/src/pages/sdk/index.mdx b/src/pages/sdk/index.mdx index e106092d..21ab6106 100644 --- a/src/pages/sdk/index.mdx +++ b/src/pages/sdk/index.mdx @@ -1,17 +1,24 @@ --- title: "SDKs and client libraries" -description: "MPP SDK implementations in TypeScript, Python, Rust, and Elixir. Libraries for building MPP clients and servers with full protocol support." +description: "Official MPP SDKs in TypeScript, Python, and Rust, plus community implementations. 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, ElixirSdkCard, WalletCliCard } from '../../components/cards' +import { TypeScriptSdkCard, PythonSdkCard, RustSdkCard, ElixirSdkCard } from '../../components/cards' -# SDKs [Official implementations in multiple languages] +# SDKs [Implementations in multiple languages] + +## Official + + +## Community + +