Skip to content
Draft
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a4144b5
feat: add html payment pages for browser 402 responses
tmm Mar 23, 2026
5c6eaf9
Merge branch 'main' of https://github.com/wevm/mppx into tmm/html-pay…
tmm Mar 23, 2026
7f390e6
chore: wip
tmm Mar 25, 2026
6884389
refactor: setup
tmm Mar 25, 2026
d22a758
chore: up
tmm Mar 25, 2026
a1ad0b6
chore: tweaks
tmm Mar 25, 2026
3ea13c3
chore: up
tmm Mar 25, 2026
feed1c4
chore: up
tmm Mar 25, 2026
f572e5f
chore: renames
tmm Mar 25, 2026
4c4bd41
wip: up
tmm Mar 26, 2026
ac05e22
chore: merge main, resolve conflicts
tmm Mar 26, 2026
a56dbed
chore: merge main, resolve middleware import conflicts
tmm Mar 26, 2026
bf6d8b2
chore: up
tmm Mar 26, 2026
8572f0b
wip: public api
tmm Mar 26, 2026
651ce54
chore: merge
tmm Mar 26, 2026
9396c6b
refactor: default off
tmm Mar 26, 2026
e999bae
chore: up
tmm Mar 26, 2026
af2f30d
chore: up
tmm Mar 26, 2026
efa6daf
chore: up
tmm Mar 26, 2026
24e783f
chore: format
tmm Mar 26, 2026
d0b758e
chore: up
tmm Mar 26, 2026
ef30de2
chore: up
tmm Mar 27, 2026
be99bf2
chore: up
tmm Mar 27, 2026
97ec923
chore: up
tmm Mar 27, 2026
6796dc6
refactor: internals
tmm Mar 27, 2026
cc43d3a
revert: remove unnecessary middleware changes
tmm Mar 27, 2026
c49e551
chore: up
tmm Mar 27, 2026
0856dd9
chore: tweaks
tmm Mar 27, 2026
7deec00
chore: tweaks
tmm Mar 27, 2026
520e076
ci: fix
tmm Mar 27, 2026
fe6ccf2
chore: up
tmm Mar 27, 2026
a52d9eb
chore: up
tmm Mar 27, 2026
3b6c8e7
chore: up
tmm Mar 27, 2026
5c31648
refactor: api design
tmm Mar 27, 2026
54ab328
chore: tweaks
tmm Mar 27, 2026
93a37e5
Merge remote-tracking branch 'origin/main' into tmm/html-payment-pages
tmm Mar 27, 2026
afe8ae9
chore: audit
tmm Mar 27, 2026
1b78777
fix: hide tempo disconnect button after submit
tmm Mar 27, 2026
30da017
Merge remote-tracking branch 'origin/main' into tmm/html-payment-pages
tmm Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ jobs:
- name: Lint & format
run: pnpm check:ci

- name: Generate HTML
run: pnpm generate:html

- name: Check types
run: pnpm dev && pnpm check:types && pnpm check:types:examples
run: pnpm check:types && pnpm check:types:html && pnpm check:types:examples

test:
name: Test Runtime
Expand Down Expand Up @@ -74,3 +77,10 @@ jobs:
pnpm run test --bail=1
env:
CI: true

- name: Run HTML e2e tests
run: pnpm test:html
env:
VITE_STRIPE_PUBLIC_KEY: ${{ secrets.VITE_STRIPE_PUBLIC_KEY }}

Check warning

Code scanning / zizmor

secrets referenced without a dedicated environment Warning

secrets referenced without a dedicated environment
VITE_STRIPE_SECRET_KEY: ${{ secrets.VITE_STRIPE_SECRET_KEY }}

Check warning

Code scanning / zizmor

secrets referenced without a dedicated environment Warning

secrets referenced without a dedicated environment
VITE_TEMPO_TAG: sha-20aecec
25 changes: 25 additions & 0 deletions .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Zizmor
on:
push:
branches: [main]
pull_request:
branches: ['**']

permissions: {}

jobs:
zizmor:
name: Zizmor
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
actions: read
steps:
- name: Clone repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Run zizmor
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ dist
node_modules
coverage
*.tsbuildinfo
*.gen.ts
.DS_Store
.env
.env.*
!.env.example
test-results/
38 changes: 34 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,42 @@ id = base64url(HMAC-SHA256(server_secret, input))
## Commands

```bash
pnpm build # Build with zile
pnpm check # Lint with oxlint + format with oxfmt
pnpm check:types # TypeScript type checking
pnpm test # Run tests with vitest
pnpm build # Build with zile
pnpm check # Lint with oxlint + format with oxfmt
pnpm check:types # TypeScript type checking
pnpm check:types:html # TypeScript type checking for HTML payment pages (browser tsconfig)
pnpm test # Run tests with vitest
pnpm test:html # Run HTML e2e tests (Stripe + Tempo) with Playwright
pnpm check:types:examples # TypeScript type checking for examples/
```

## HTML Payment Pages

Browser-rendered payment pages live in `src/html/`. Each method (Tempo, Stripe) has its own directory with:

- `src/charge.ts` — Entry point, creates DOM and handles payment flow
- `src/env.d.ts` — Module augmentation for `MppxConfig` and `MppxChallengeRequest`
- `vite.config.ts` — Dev server and build config

### Build pipeline

`pnpm build` bundles each method's `charge.ts` into `{method}/server/internal/html.gen.ts` (generated, do not edit). The page shell is bundled into `server/internal/html.gen.ts`.

### Global types

- `src/html/env.d.ts` — Base global types (`mppx` var, `MppxConfig`, `MppxChallengeRequest`, `MppxEventMap`)
- Each method augments `MppxConfig` and `MppxChallengeRequest` via its own `src/env.d.ts`
- Browser tsconfig: `src/html/tsconfig.browser.json`

### Infrastructure routes (`mppx.html()`)

`Mppx.create()` returns an `html(request)` method that handles infrastructure routes:

- Service worker (`/__mppx_serviceWorker.js`)
- Method-registered routes (e.g., Stripe's `/__mppx_stripe_create_token`)

Methods register routes via `htmlRoutes` on `Method.toServer()`.

## Skills Reference

Load these skills for specialized guidance:
Expand Down
4 changes: 2 additions & 2 deletions examples/charge/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { tempoModerato } from 'viem/chains'
import { Actions } from 'viem/tempo'

const account = privateKeyToAccount(generatePrivateKey())
const currency = '0x20c0000000000000000000000000000000000000' as const // pathUSD

const mppx = Mppx.create({
methods: [
tempo({
currency,
currency: '0x20c0000000000000000000000000000000000000',
feePayer: true,
html: true,
recipient: account.address,
testnet: true,
}),
Expand Down
78 changes: 5 additions & 73 deletions examples/stripe/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const mppx = Mppx.create({
methods: [
stripe.charge({
client: stripeClient,
html: {
// Publishable key for browser HTML payment form.
publishableKey: process.env.VITE_STRIPE_PUBLIC_KEY!,
},
// Stripe Business Network profile ID.
networkId: 'internal',
// Ensure only card is supported.
Expand All @@ -17,80 +21,8 @@ const mppx = Mppx.create({
],
})

// Handles creating an SPT and charging a customer.
// In production examples, this would be a DIFFERENT server than
// the one that handles the HTTP 402 flow.
export async function handler(request: Request): Promise<Response | null> {
const url = new URL(request.url)

if (url.pathname === '/api/create-spt') {
const { paymentMethod, amount, currency, expiresAt, networkId, metadata } =
(await request.json()) as {
paymentMethod: string
amount: string
currency: string
expiresAt: number
networkId?: string
metadata?: Record<string, string>
}

if (metadata?.externalId) {
return Response.json(
{ error: 'metadata.externalId is reserved; use credential externalId instead' },
{ status: 400 },
)
}

const body = new URLSearchParams({
payment_method: paymentMethod,
'usage_limits[currency]': currency,
'usage_limits[max_amount]': amount,
'usage_limits[expires_at]': expiresAt.toString(),
})
if (networkId) body.set('seller_details[network_id]', networkId)
if (metadata) {
for (const [key, value] of Object.entries(metadata)) {
body.set(`metadata[${key}]`, value)
}
}

// Test-only endpoint; production SPT flow uses the agent-side issued_tokens API.
const createSpt = async (bodyParams: URLSearchParams) =>
fetch('https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens', {
method: 'POST',
headers: {
Authorization: `Basic ${btoa(`${secretKey}:`)}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: bodyParams,
})

let response = await createSpt(body)
if (!response.ok) {
const error = (await response.json()) as { error: { message: string } }
if ((metadata || networkId) && error.error.message.includes('Received unknown parameter')) {
const fallbackBody = new URLSearchParams({
payment_method: paymentMethod,
'usage_limits[currency]': currency,
'usage_limits[max_amount]': amount,
'usage_limits[expires_at]': expiresAt.toString(),
})
response = await createSpt(fallbackBody)
} else {
return Response.json({ error: error.error.message }, { status: 500 })
}
}

if (!response.ok) {
const error = (await response.json()) as { error: { message: string } }
return Response.json({ error: error.error.message }, { status: 500 })
}

const { id: spt } = (await response.json()) as { id: string }
return Response.json({ spt })
}

if (url.pathname === '/api/fortune') {
if (new URL(request.url).pathname === '/api/fortune') {
const result = await mppx.charge({
amount: '1',
currency: 'usd',
Expand Down
38 changes: 33 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
{
"scripts": {
"build": "zile",
"changeset:publish": "zile publish:prepare && changeset publish && zile publish:post",
"build": "pnpm generate:html && zile",
"build:html": "vp build src/html/page && vp build src/html/tempo && vp build src/html/stripe",
"changeset:publish": "pnpm generate:html && zile publish:prepare && changeset publish && zile publish:post",
"changeset:version": "changeset version",
"check": "vp lint --fix && vp fmt --write .",
"check:ci": "vp lint && vp fmt --check .",
"check:types": "tsgo -b",
"check:types:html": "tsgo -p src/html/tsconfig.browser.json",
"check:types:examples": "pnpm -r --filter './examples/**' run check:types",
"deps": "pnpx taze -r --no-ignore-other-workspaces --ignore-paths node_modules",
"deps:ci": "pnpx actions-up",
"dev": "zile dev",
"dev:example": "node scripts/dev:example.ts",
"dev": "pnpm generate:html && zile dev",
"dev:example": "pnpm generate:html && node scripts/dev:example.ts",
"dev:html:compose": "pnpm --filter @mppx/html-page dev:compose",
"dev:html:stripe": "pnpm --filter @mppx/html-stripe dev",
"dev:html:tempo": "pnpm --filter @mppx/html-tempo dev",
"generate:html": "pnpm build:html",
"mppx": "node --import tsx src/bin.ts",
"test": "vp test"
"test": "pnpm generate:html && vp test",
"test:html": "pnpm generate:html && pnpm test:html:form && pnpm test:html:compose && pnpm test:html:stripe && pnpm test:html:tempo",
"test:html:compose": "pnpm --filter @mppx/html-page test:compose",
"test:html:form": "playwright test -c src/html/test/playwright.config.ts",
"test:html:stripe": "pnpm --filter @mppx/html-stripe test",
"test:html:tempo": "pnpm --filter @mppx/html-tempo test"
},
"browserslist": [
"defaults",
Expand All @@ -39,13 +50,16 @@
"fast-check": "^4.6.0",
"file-type": "^21.3.2",
"hono": "^4.11.9",
"@playwright/test": "^1.58.2",
"playwright": "^1.58.2",
"prool": "^0.2.4",
"vite": "^8.0.0",
"tempo.ts": "^0.14.2",
"testcontainers": "^11.11.0",
"tsx": "^4.21.0",
"typescript": "~5.9.3",
"viem": "^2.47.6",
"vite-plus": "~0.1.14",
"vp": "npm:vite-plus@~0.1.14",
"zile": "^0.0.19"
},
Expand Down Expand Up @@ -146,6 +160,20 @@
"types": "./dist/middlewares/elysia.d.ts",
"src": "./src/middlewares/elysia.ts",
"default": "./dist/middlewares/elysia.js"
},
"./html": {
"types": "./dist/html/index.d.ts",
"src": "./src/html/index.ts",
"default": "./dist/html/index.js"
},
"./html/vite": {
"types": "./dist/html/vite.d.ts",
"src": "./src/html/vite.ts",
"default": "./dist/html/vite.js"
},
"./html/env": {
"types": "./dist/html/env.d.ts",
"src": "./src/html/env.d.ts"
}
},
"peerDependencies": {
Expand Down
Loading
Loading