diff --git a/src/components/cards.tsx b/src/components/cards.tsx index 0e25a18b..d1e1662e 100644 --- a/src/components/cards.tsx +++ b/src/components/cards.tsx @@ -121,6 +121,18 @@ export function RustSdkCard() { ); } +export function ElixirSdkCard() { + return ( + Community} + /> + ); +} + export function MppxCreateReferenceCard({ to }: { to: string }) { return ( + + + + + + 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`: + +```elixir [mix.exs] +defp deps do + [ + {:mpp, "~> 0.1"} # check hex.pm for latest version + ] +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"), + "network_id" => System.get_env("STRIPE_NETWORK_ID") + }, + 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, + 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, + method_config: stripe_config + 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 +- [Full API docs](https://hexdocs.pm/mpp): Complete module reference on HexDocs 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..21ab6106 100644 --- a/src/pages/sdk/index.mdx +++ b/src/pages/sdk/index.mdx @@ -1,16 +1,24 @@ --- 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: "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, 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 + + + + 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" }, + ], + }, ], }, {