/** * Stripe Shared Payment Token (SPT) creation endpoint. * * Production agents obtain SPTs directly from Stripe via the Link CLI or the * `issued_tokens` API — they never need this endpoint. * * For browser-based / test flows, this endpoint lets Stripe.js mint an SPT * server-side so the full 402 flow can be exercised locally without the agent * CLI. * * POST /api/stripe-proxy/create-spt * Body (JSON): * paymentMethod string – Stripe PaymentMethod ID (pm_…) * amount string – Max charge in minor units, e.g. "100" = $1.00 * currency string – ISO 4217 code, e.g. "usd" * expiresAt number – Unix timestamp for SPT expiry * metadata? Record – Forwarded to the PaymentIntent as metadata * Response (JSON): { spt: "spt_…" } * * Note: seller_details[network_id] is always set from the server's configured * Stripe profile ID — the client cannot override which seller receives the SPT. */ export type CreateSptBody = { paymentMethod: string amount: string currency: string expiresAt: number metadata?: Record } /** * Handle a POST /api/stripe-proxy/create-spt request using the Stripe test-helper API. * * @param req Incoming request * @param stripeSecretKey Stripe secret key for the Basic-auth header * @param networkId Server's Stripe Business Network profile ID — always * injected as seller_details[network_id] so the SPT is * scoped to the correct seller account. * * NOTE: The test-helper endpoint only exists in Stripe test mode (sk_test_…). * In production, clients use the `issued_tokens` API — this function is only * needed for local development and demos. */ export async function handleCreateSpt( req: Request, stripeSecretKey: string, networkId: string, ): Promise { let body: CreateSptBody try { body = (await req.json()) as CreateSptBody } catch { return Response.json({ error: 'Invalid JSON body' }, { status: 400 }) } const { paymentMethod, amount, currency, expiresAt, metadata } = body if (!paymentMethod || !amount || !currency || !expiresAt) { return Response.json( { error: 'Required fields: paymentMethod, amount, currency, expiresAt' }, { status: 400 }, ) } const params = new URLSearchParams({ payment_method: paymentMethod, 'usage_limits[currency]': currency, 'usage_limits[max_amount]': amount, 'usage_limits[expires_at]': String(expiresAt), // Always set from server config — clients cannot redirect the SPT to a // different seller by sending their own networkId in the request body. 'seller_details[network_id]': networkId, }) if (metadata) { for (const [key, value] of Object.entries(metadata)) { params.set(`metadata[${key}]`, value) } } const stripeTestHelperUrl = 'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens' const authHeader = `Basic ${btoa(`${stripeSecretKey}:`)}` // Stripe test-only endpoint for granting SPTs in development const res = await fetch(stripeTestHelperUrl, { method: 'POST', headers: { Authorization: authHeader, 'Content-Type': 'application/x-www-form-urlencoded' }, body: params, }) if (!res.ok) { const errBody = (await res.json()) as { error: { message: string } } // The test-helper may not yet support seller_details or metadata on older // preview versions — retry with only the required fields. if (errBody.error.message.includes('Received unknown parameter')) { const fallback = new URLSearchParams({ payment_method: paymentMethod, 'usage_limits[currency]': currency, 'usage_limits[max_amount]': amount, 'usage_limits[expires_at]': String(expiresAt), }) const retry = await fetch(stripeTestHelperUrl, { method: 'POST', headers: { Authorization: authHeader, 'Content-Type': 'application/x-www-form-urlencoded' }, body: fallback, }) if (!retry.ok) { const retryErr = (await retry.json()) as { error: { message: string } } return Response.json({ error: retryErr.error.message }, { status: 500 }) } const { id: spt } = (await retry.json()) as { id: string } return Response.json({ spt }) } return Response.json({ error: errBody.error.message }, { status: 500 }) } const { id: spt } = (await res.json()) as { id: string } return Response.json({ spt }) }