mppx 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/dist/Challenge.d.ts +16 -16
  2. package/dist/Challenge.d.ts.map +1 -1
  3. package/dist/Challenge.js +7 -7
  4. package/dist/Challenge.js.map +1 -1
  5. package/dist/Errors.d.ts +58 -8
  6. package/dist/Errors.d.ts.map +1 -1
  7. package/dist/Errors.js +51 -9
  8. package/dist/Errors.js.map +1 -1
  9. package/dist/Method.d.ts +154 -0
  10. package/dist/Method.d.ts.map +1 -0
  11. package/dist/Method.js +81 -0
  12. package/dist/Method.js.map +1 -0
  13. package/dist/PaymentRequest.d.ts +5 -5
  14. package/dist/PaymentRequest.d.ts.map +1 -1
  15. package/dist/PaymentRequest.js +5 -5
  16. package/dist/cli.js +67 -18
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client/Methods.d.ts +2 -2
  19. package/dist/client/Methods.d.ts.map +1 -1
  20. package/dist/client/Methods.js +2 -2
  21. package/dist/client/Methods.js.map +1 -1
  22. package/dist/client/Mppx.d.ts +7 -7
  23. package/dist/client/Mppx.d.ts.map +1 -1
  24. package/dist/client/Mppx.js +3 -3
  25. package/dist/client/Mppx.js.map +1 -1
  26. package/dist/client/internal/Fetch.d.ts +10 -10
  27. package/dist/client/internal/Fetch.d.ts.map +1 -1
  28. package/dist/client/internal/Fetch.js +2 -2
  29. package/dist/client/internal/Fetch.js.map +1 -1
  30. package/dist/index.d.ts +1 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/mcp-sdk/client/McpClient.d.ts +6 -6
  35. package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
  36. package/dist/mcp-sdk/client/McpClient.js +4 -4
  37. package/dist/mcp-sdk/client/McpClient.js.map +1 -1
  38. package/dist/middlewares/elysia.d.ts +1 -1
  39. package/dist/middlewares/express.d.ts +1 -1
  40. package/dist/middlewares/hono.d.ts +1 -1
  41. package/dist/middlewares/internal/mppx.d.ts +7 -7
  42. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  43. package/dist/middlewares/internal/mppx.js +5 -5
  44. package/dist/middlewares/internal/mppx.js.map +1 -1
  45. package/dist/middlewares/nextjs.d.ts +1 -1
  46. package/dist/proxy/Service.js +2 -2
  47. package/dist/proxy/Service.js.map +1 -1
  48. package/dist/server/Methods.d.ts +2 -2
  49. package/dist/server/Methods.d.ts.map +1 -1
  50. package/dist/server/Methods.js +2 -2
  51. package/dist/server/Methods.js.map +1 -1
  52. package/dist/server/Mppx.d.ts +17 -17
  53. package/dist/server/Mppx.d.ts.map +1 -1
  54. package/dist/server/Mppx.js +9 -9
  55. package/dist/server/Mppx.js.map +1 -1
  56. package/dist/stripe/{Intents.d.ts → Methods.d.ts} +22 -22
  57. package/dist/stripe/Methods.d.ts.map +1 -0
  58. package/dist/stripe/Methods.js +42 -0
  59. package/dist/stripe/Methods.js.map +1 -0
  60. package/dist/stripe/client/Charge.d.ts +40 -27
  61. package/dist/stripe/client/Charge.d.ts.map +1 -1
  62. package/dist/stripe/client/Charge.js +15 -7
  63. package/dist/stripe/client/Charge.js.map +1 -1
  64. package/dist/stripe/client/{MethodIntents.d.ts → Methods.d.ts} +24 -23
  65. package/dist/stripe/client/Methods.d.ts.map +1 -0
  66. package/dist/stripe/client/{MethodIntents.js → Methods.js} +3 -3
  67. package/dist/stripe/client/Methods.js.map +1 -0
  68. package/dist/stripe/client/index.d.ts +1 -1
  69. package/dist/stripe/client/index.d.ts.map +1 -1
  70. package/dist/stripe/client/index.js +1 -1
  71. package/dist/stripe/client/index.js.map +1 -1
  72. package/dist/stripe/index.d.ts +1 -1
  73. package/dist/stripe/index.d.ts.map +1 -1
  74. package/dist/stripe/index.js +1 -1
  75. package/dist/stripe/index.js.map +1 -1
  76. package/dist/stripe/internal/types.d.ts +25 -0
  77. package/dist/stripe/internal/types.d.ts.map +1 -0
  78. package/dist/stripe/internal/types.js +2 -0
  79. package/dist/stripe/internal/types.js.map +1 -0
  80. package/dist/stripe/server/Charge.d.ts +47 -28
  81. package/dist/stripe/server/Charge.d.ts.map +1 -1
  82. package/dist/stripe/server/Charge.js +90 -32
  83. package/dist/stripe/server/Charge.js.map +1 -1
  84. package/dist/stripe/server/{MethodIntents.d.ts → Methods.d.ts} +24 -23
  85. package/dist/stripe/server/Methods.d.ts.map +1 -0
  86. package/dist/stripe/server/{MethodIntents.js → Methods.js} +3 -3
  87. package/dist/stripe/server/Methods.js.map +1 -0
  88. package/dist/stripe/server/index.d.ts +1 -1
  89. package/dist/stripe/server/index.d.ts.map +1 -1
  90. package/dist/stripe/server/index.js +1 -1
  91. package/dist/stripe/server/index.js.map +1 -1
  92. package/dist/tempo/{Intents.d.ts → Methods.d.ts} +72 -69
  93. package/dist/tempo/Methods.d.ts.map +1 -0
  94. package/dist/tempo/Methods.js +118 -0
  95. package/dist/tempo/Methods.js.map +1 -0
  96. package/dist/tempo/client/ChannelOps.d.ts +1 -1
  97. package/dist/tempo/client/ChannelOps.js +1 -1
  98. package/dist/tempo/client/Charge.d.ts +25 -25
  99. package/dist/tempo/client/Charge.d.ts.map +1 -1
  100. package/dist/tempo/client/Charge.js +3 -3
  101. package/dist/tempo/client/Charge.js.map +1 -1
  102. package/dist/tempo/client/{MethodIntents.d.ts → Methods.d.ts} +74 -70
  103. package/dist/tempo/client/Methods.d.ts.map +1 -0
  104. package/dist/tempo/client/{MethodIntents.js → Methods.js} +3 -3
  105. package/dist/tempo/client/Methods.js.map +1 -0
  106. package/dist/tempo/client/Session.d.ts +49 -45
  107. package/dist/tempo/client/Session.d.ts.map +1 -1
  108. package/dist/tempo/client/Session.js +4 -4
  109. package/dist/tempo/client/Session.js.map +1 -1
  110. package/dist/tempo/client/SessionManager.d.ts +1 -1
  111. package/dist/tempo/client/SessionManager.js +1 -1
  112. package/dist/tempo/client/index.d.ts +1 -1
  113. package/dist/tempo/client/index.d.ts.map +1 -1
  114. package/dist/tempo/client/index.js +1 -1
  115. package/dist/tempo/client/index.js.map +1 -1
  116. package/dist/tempo/index.d.ts +1 -1
  117. package/dist/tempo/index.d.ts.map +1 -1
  118. package/dist/tempo/index.js +1 -1
  119. package/dist/tempo/index.js.map +1 -1
  120. package/dist/tempo/server/Charge.d.ts +27 -27
  121. package/dist/tempo/server/Charge.d.ts.map +1 -1
  122. package/dist/tempo/server/Charge.js +3 -3
  123. package/dist/tempo/server/Charge.js.map +1 -1
  124. package/dist/tempo/server/{MethodIntents.d.ts → Methods.d.ts} +73 -69
  125. package/dist/tempo/server/Methods.d.ts.map +1 -0
  126. package/dist/tempo/server/{MethodIntents.js → Methods.js} +4 -4
  127. package/dist/tempo/server/Methods.js.map +1 -0
  128. package/dist/tempo/server/Session.d.ts +51 -47
  129. package/dist/tempo/server/Session.d.ts.map +1 -1
  130. package/dist/tempo/server/Session.js +4 -4
  131. package/dist/tempo/server/Session.js.map +1 -1
  132. package/dist/tempo/server/index.d.ts +6 -0
  133. package/dist/tempo/server/index.d.ts.map +1 -0
  134. package/dist/tempo/server/index.js +6 -0
  135. package/dist/tempo/server/index.js.map +1 -0
  136. package/package.json +1 -1
  137. package/src/Challenge.test-d.ts +3 -3
  138. package/src/Challenge.test.ts +6 -6
  139. package/src/Challenge.ts +32 -32
  140. package/src/Errors.test.ts +75 -21
  141. package/src/Errors.ts +74 -9
  142. package/src/Method.test.ts +76 -0
  143. package/src/Method.ts +228 -0
  144. package/src/PaymentRequest.test.ts +4 -4
  145. package/src/PaymentRequest.ts +9 -9
  146. package/src/cli.test.ts +12 -22
  147. package/src/cli.ts +74 -21
  148. package/src/client/Methods.ts +2 -2
  149. package/src/client/Mppx.test-d.ts +6 -6
  150. package/src/client/Mppx.test.ts +26 -22
  151. package/src/client/Mppx.ts +10 -10
  152. package/src/client/Transport.test.ts +2 -2
  153. package/src/client/internal/Fetch.ts +21 -24
  154. package/src/index.ts +1 -2
  155. package/src/mcp-sdk/client/McpClient.ts +11 -13
  156. package/src/middlewares/elysia.ts +1 -1
  157. package/src/middlewares/express.ts +1 -1
  158. package/src/middlewares/hono.ts +1 -1
  159. package/src/middlewares/internal/mppx.ts +10 -10
  160. package/src/middlewares/nextjs.ts +1 -1
  161. package/src/proxy/Service.ts +2 -2
  162. package/src/server/Methods.ts +2 -2
  163. package/src/server/Mppx.test-d.ts +27 -29
  164. package/src/server/Mppx.test.ts +23 -19
  165. package/src/server/Mppx.ts +43 -43
  166. package/src/server/Transport.test.ts +3 -3
  167. package/src/stripe/{Intents.test.ts → Methods.test.ts} +12 -12
  168. package/src/stripe/Methods.ts +45 -0
  169. package/src/stripe/client/Charge.test.ts +189 -0
  170. package/src/stripe/client/Charge.ts +29 -16
  171. package/src/stripe/client/{MethodIntents.ts → Methods.ts} +2 -2
  172. package/src/stripe/client/index.ts +1 -1
  173. package/src/stripe/index.ts +1 -1
  174. package/src/stripe/internal/types.ts +22 -0
  175. package/src/stripe/server/Charge.test.ts +241 -0
  176. package/src/stripe/server/Charge.ts +124 -38
  177. package/src/stripe/server/{MethodIntents.ts → Methods.ts} +2 -2
  178. package/src/stripe/server/index.ts +1 -1
  179. package/src/tempo/{Intents.test.ts → Methods.test.ts} +15 -15
  180. package/src/tempo/{Intents.ts → Methods.ts} +77 -22
  181. package/src/tempo/client/ChannelOps.ts +1 -1
  182. package/src/tempo/client/Charge.ts +3 -3
  183. package/src/tempo/client/{MethodIntents.ts → Methods.ts} +2 -2
  184. package/src/tempo/client/Session.ts +4 -4
  185. package/src/tempo/client/SessionManager.ts +1 -1
  186. package/src/tempo/client/index.ts +1 -1
  187. package/src/tempo/index.ts +1 -1
  188. package/src/tempo/server/Charge.ts +4 -7
  189. package/src/tempo/server/{MethodIntents.ts → Methods.ts} +3 -3
  190. package/src/tempo/server/Session.test.ts +4 -7
  191. package/src/tempo/server/Session.ts +6 -6
  192. package/src/tempo/server/index.ts +1 -1
  193. package/dist/Intent.d.ts +0 -101
  194. package/dist/Intent.d.ts.map +0 -1
  195. package/dist/Intent.js +0 -83
  196. package/dist/Intent.js.map +0 -1
  197. package/dist/MethodIntent.d.ts +0 -225
  198. package/dist/MethodIntent.d.ts.map +0 -1
  199. package/dist/MethodIntent.js +0 -156
  200. package/dist/MethodIntent.js.map +0 -1
  201. package/dist/stripe/Intents.d.ts.map +0 -1
  202. package/dist/stripe/Intents.js +0 -27
  203. package/dist/stripe/Intents.js.map +0 -1
  204. package/dist/stripe/client/MethodIntents.d.ts.map +0 -1
  205. package/dist/stripe/client/MethodIntents.js.map +0 -1
  206. package/dist/stripe/server/MethodIntents.d.ts.map +0 -1
  207. package/dist/stripe/server/MethodIntents.js.map +0 -1
  208. package/dist/tempo/Intents.d.ts.map +0 -1
  209. package/dist/tempo/Intents.js +0 -81
  210. package/dist/tempo/Intents.js.map +0 -1
  211. package/dist/tempo/client/MethodIntents.d.ts.map +0 -1
  212. package/dist/tempo/client/MethodIntents.js.map +0 -1
  213. package/dist/tempo/server/MethodIntents.d.ts.map +0 -1
  214. package/dist/tempo/server/MethodIntents.js.map +0 -1
  215. package/src/Intent.test.ts +0 -180
  216. package/src/Intent.ts +0 -109
  217. package/src/MethodIntent.test.ts +0 -303
  218. package/src/MethodIntent.ts +0 -388
  219. package/src/stripe/Intents.ts +0 -27
@@ -0,0 +1,45 @@
1
+ import { parseUnits } from 'viem'
2
+ import * as Expires from '../Expires.js'
3
+ import * as Method from '../Method.js'
4
+ import * as z from '../zod.js'
5
+
6
+ /**
7
+ * Stripe charge intent for one-time payments via Shared Payment Tokens (SPTs).
8
+ *
9
+ * @see https://github.com/tempoxyz/payment-auth-spec/blob/main/specs/methods/stripe/draft-stripe-charge-00.md
10
+ */
11
+ export const charge = Method.from({
12
+ name: 'stripe',
13
+ intent: 'charge',
14
+ schema: {
15
+ credential: {
16
+ payload: z.object({
17
+ externalId: z.optional(z.string()),
18
+ spt: z.string(),
19
+ }),
20
+ },
21
+ request: z.pipe(
22
+ z.object({
23
+ amount: z.amount(),
24
+ currency: z.string(),
25
+ decimals: z.number(),
26
+ description: z.optional(z.string()),
27
+ expires: z._default(z.datetime(), () => Expires.minutes(5)),
28
+ externalId: z.optional(z.string()),
29
+ metadata: z.optional(z.record(z.string(), z.string())),
30
+ networkId: z.string(),
31
+ paymentMethodTypes: z.array(z.string()).check(z.minLength(1)),
32
+ recipient: z.optional(z.string()),
33
+ }),
34
+ z.transform(({ amount, decimals, metadata, networkId, paymentMethodTypes, ...rest }) => ({
35
+ ...rest,
36
+ amount: parseUnits(amount, decimals).toString(),
37
+ methodDetails: {
38
+ networkId,
39
+ paymentMethodTypes,
40
+ ...(metadata !== undefined && { metadata }),
41
+ },
42
+ })),
43
+ ),
44
+ },
45
+ })
@@ -0,0 +1,189 @@
1
+ import { Challenge, Credential } from 'mppx'
2
+ import { Mppx, stripe } from 'mppx/client'
3
+ import { Mppx as Mppx_server, stripe as stripe_server } from 'mppx/server'
4
+ import { describe, expect, test, vi } from 'vitest'
5
+ import type { StripeJs } from '../internal/types.js'
6
+ import { charge as clientCharge_ } from './Charge.js'
7
+
8
+ const realm = 'api.example.com'
9
+ const secretKey = 'test-hmac-key'
10
+
11
+ const dummyClientCharge = clientCharge_({
12
+ createToken: async () => 'spt_test',
13
+ paymentMethod: 'pm_test',
14
+ })
15
+
16
+ async function createChallenge() {
17
+ const server = Mppx_server.create({
18
+ methods: [
19
+ stripe_server.charge({
20
+ networkId: 'internal',
21
+ paymentMethodTypes: ['card'],
22
+ secretKey: 'sk_test',
23
+ }),
24
+ ],
25
+ realm,
26
+ secretKey,
27
+ })
28
+
29
+ const handle = server.charge({ amount: '100', currency: 'usd', decimals: 2 })
30
+ const result = await handle(new Request('https://example.com'))
31
+ if (result.status !== 402) throw new Error('Expected 402')
32
+ return Challenge.fromResponse(result.challenge, { methods: [dummyClientCharge] })
33
+ }
34
+
35
+ function createMockStripeJs(): StripeJs {
36
+ return {
37
+ createPaymentMethod: vi.fn(async () => ({
38
+ error: null,
39
+ paymentMethod: { id: 'pm_mock_123' },
40
+ })),
41
+ elements: vi.fn(() => ({})),
42
+ }
43
+ }
44
+
45
+ describe('stripe.charge client param', () => {
46
+ test('default: forwards client to createToken callback', async () => {
47
+ const mockClient = createMockStripeJs()
48
+ let receivedClient: StripeJs | undefined
49
+
50
+ const charge = stripe.charge({
51
+ client: mockClient,
52
+ createToken: async (params) => {
53
+ receivedClient = params.client
54
+ return 'spt_test_123'
55
+ },
56
+ paymentMethod: 'pm_card_visa',
57
+ })
58
+
59
+ const challenge = await createChallenge()
60
+ await charge.createCredential({ challenge, context: {} })
61
+
62
+ expect(receivedClient).toBe(mockClient)
63
+ })
64
+
65
+ test('behavior: client is undefined when not provided', async () => {
66
+ let receivedClient: StripeJs | undefined = createMockStripeJs()
67
+
68
+ const charge = stripe.charge({
69
+ createToken: async (params) => {
70
+ receivedClient = params.client
71
+ return 'spt_test_123'
72
+ },
73
+ paymentMethod: 'pm_card_visa',
74
+ })
75
+
76
+ const challenge = await createChallenge()
77
+ await charge.createCredential({ challenge, context: {} })
78
+
79
+ expect(receivedClient).toBeUndefined()
80
+ })
81
+
82
+ test('behavior: createToken receives all expected params', async () => {
83
+ const mockClient = createMockStripeJs()
84
+ let receivedParams: Record<string, unknown> | undefined
85
+
86
+ const charge = stripe.charge({
87
+ client: mockClient,
88
+ createToken: async (params) => {
89
+ receivedParams = params as unknown as Record<string, unknown>
90
+ return 'spt_test_123'
91
+ },
92
+ paymentMethod: 'pm_card_visa',
93
+ })
94
+
95
+ const challenge = await createChallenge()
96
+ await charge.createCredential({ challenge, context: {} })
97
+
98
+ expect(receivedParams).toBeDefined()
99
+ expect(receivedParams!.amount).toBe('10000')
100
+ expect(receivedParams!.currency).toBe('usd')
101
+ expect(receivedParams!.networkId).toBe('internal')
102
+ expect(receivedParams!.paymentMethod).toBe('pm_card_visa')
103
+ expect(receivedParams!.client).toBe(mockClient)
104
+ expect(receivedParams!.challenge).toBeDefined()
105
+ expect(typeof receivedParams!.expiresAt).toBe('number')
106
+ })
107
+
108
+ test('behavior: produces valid credential string', async () => {
109
+ const charge = stripe.charge({
110
+ createToken: async () => 'spt_test_123',
111
+ paymentMethod: 'pm_card_visa',
112
+ })
113
+
114
+ const challenge = await createChallenge()
115
+ const credential = await charge.createCredential({ challenge, context: {} })
116
+
117
+ expect(credential).toMatch(/^Payment /)
118
+
119
+ const parsed = Credential.deserialize(credential)
120
+ expect(parsed.payload).toMatchObject({ spt: 'spt_test_123' })
121
+ })
122
+
123
+ test('behavior: includes externalId in credential payload', async () => {
124
+ const charge = stripe.charge({
125
+ createToken: async () => 'spt_test_123',
126
+ externalId: 'order_456',
127
+ paymentMethod: 'pm_card_visa',
128
+ })
129
+
130
+ const challenge = await createChallenge()
131
+ const credential = await charge.createCredential({ challenge, context: {} })
132
+ const parsed = Credential.deserialize(credential)
133
+ expect(parsed.payload).toMatchObject({
134
+ externalId: 'order_456',
135
+ spt: 'spt_test_123',
136
+ })
137
+ })
138
+
139
+ test('behavior: context paymentMethod overrides default', async () => {
140
+ let receivedPaymentMethod: string | undefined
141
+
142
+ const charge = stripe.charge({
143
+ createToken: async (params) => {
144
+ receivedPaymentMethod = params.paymentMethod
145
+ return 'spt_test_123'
146
+ },
147
+ paymentMethod: 'pm_default',
148
+ })
149
+
150
+ const challenge = await createChallenge()
151
+ await charge.createCredential({
152
+ challenge,
153
+ context: { paymentMethod: 'pm_override' },
154
+ })
155
+
156
+ expect(receivedPaymentMethod).toBe('pm_override')
157
+ })
158
+
159
+ test('behavior: Mppx.create with client forwards through createCredential', async () => {
160
+ const mockClient = createMockStripeJs()
161
+ let receivedClient: StripeJs | undefined
162
+
163
+ const mppx = Mppx.create({
164
+ methods: [
165
+ stripe.charge({
166
+ client: mockClient,
167
+ createToken: async (params) => {
168
+ receivedClient = params.client
169
+ return 'spt_test_123'
170
+ },
171
+ paymentMethod: 'pm_card_visa',
172
+ }),
173
+ ],
174
+ polyfill: false,
175
+ })
176
+
177
+ const challenge = await createChallenge()
178
+ const response = new Response(null, {
179
+ status: 402,
180
+ headers: {
181
+ 'WWW-Authenticate': Challenge.serialize(challenge),
182
+ },
183
+ })
184
+
185
+ await mppx.createCredential(response)
186
+
187
+ expect(receivedClient).toBe(mockClient)
188
+ })
189
+ })
@@ -1,8 +1,9 @@
1
1
  import type * as Challenge from '../../Challenge.js'
2
2
  import * as Credential from '../../Credential.js'
3
- import * as MethodIntent from '../../MethodIntent.js'
3
+ import * as Method from '../../Method.js'
4
4
  import * as z from '../../zod.js'
5
- import * as Intents from '../Intents.js'
5
+ import type { StripeJs } from '../internal/types.js'
6
+ import * as Methods from '../Methods.js'
6
7
 
7
8
  /**
8
9
  * Creates a Stripe charge method intent for usage on the client.
@@ -14,11 +15,18 @@ import * as Intents from '../Intents.js'
14
15
  * The `paymentMethod` (e.g. from Stripe Elements) can be provided at
15
16
  * initialization or at credential-creation time via `context`.
16
17
  *
18
+ * Optionally accepts a `client` (a Stripe.js instance from `@stripe/stripe-js`)
19
+ * which is forwarded to the `createToken` callback for use with Elements.
20
+ *
17
21
  * @example
18
22
  * ```ts
23
+ * import { loadStripe } from '@stripe/stripe-js'
19
24
  * import { stripe } from 'mppx/client'
20
25
  *
26
+ * const stripeJs = await loadStripe('pk_...')
27
+ *
21
28
  * const charge = stripe.charge({
29
+ * client: stripeJs,
22
30
  * createToken: async ({ amount, currency, expiresAt, metadata, networkId, paymentMethod }) => {
23
31
  * const res = await fetch('/api/create-spt', {
24
32
  * method: 'POST',
@@ -32,9 +40,9 @@ import * as Intents from '../Intents.js'
32
40
  * ```
33
41
  */
34
42
  export function charge(parameters: charge.Parameters) {
35
- const { createToken, externalId, paymentMethod: defaultPaymentMethod } = parameters
43
+ const { client, createToken, externalId, paymentMethod: defaultPaymentMethod } = parameters
36
44
 
37
- return MethodIntent.toClient(Intents.charge, {
45
+ return Method.toClient(Methods.charge, {
38
46
  context: z.object({
39
47
  paymentMethod: z.optional(z.string()),
40
48
  }),
@@ -63,13 +71,14 @@ export function charge(parameters: charge.Parameters) {
63
71
  : Math.floor(Date.now() / 1000) + 3600
64
72
 
65
73
  const spt = await createToken({
66
- challenge,
67
- paymentMethod,
68
74
  amount,
75
+ challenge,
76
+ client,
69
77
  currency,
70
- networkId,
71
78
  expiresAt,
72
79
  metadata,
80
+ networkId,
81
+ paymentMethod,
73
82
  })
74
83
 
75
84
  return Credential.serialize({
@@ -85,6 +94,8 @@ export function charge(parameters: charge.Parameters) {
85
94
 
86
95
  export declare namespace charge {
87
96
  type Parameters = {
97
+ /** Stripe.js instance from `@stripe/stripe-js`. Forwarded to `createToken` for use with Elements. */
98
+ client?: StripeJs | undefined
88
99
  /** Called when a Stripe challenge is received. Create an SPT to retry. */
89
100
  createToken: (parameters: OnChallengeParameters) => Promise<string>
90
101
  /** Optional client-side external reference ID for the credential payload. */
@@ -94,22 +105,24 @@ export declare namespace charge {
94
105
  }
95
106
 
96
107
  type OnChallengeParameters = {
97
- challenge: Challenge.Challenge<
98
- z.output<typeof Intents.charge.schema.request>,
99
- typeof Intents.charge.name,
100
- typeof Intents.charge.method
101
- >
102
- /** Stripe payment method ID (e.g. from Stripe Elements). */
103
- paymentMethod?: string | undefined
104
108
  /** Payment amount (in smallest currency unit). */
105
109
  amount: string
110
+ challenge: Challenge.Challenge<
111
+ z.output<typeof Methods.charge.schema.request>,
112
+ typeof Methods.charge.intent,
113
+ typeof Methods.charge.name
114
+ >
115
+ /** Stripe.js instance, if provided to `stripe.charge()`. */
116
+ client?: StripeJs | undefined
106
117
  /** Three-letter ISO currency code. */
107
118
  currency: string
108
- /** Stripe Business Network profile ID. */
109
- networkId: string | undefined
110
119
  /** SPT expiration as a Unix timestamp (seconds). */
111
120
  expiresAt: number
112
121
  /** Optional metadata to associate with the SPT. */
113
122
  metadata?: Record<string, string> | undefined
123
+ /** Stripe Business Network profile ID. */
124
+ networkId: string | undefined
125
+ /** Stripe payment method ID (e.g. from Stripe Elements). */
126
+ paymentMethod?: string | undefined
114
127
  }
115
128
  }
@@ -1,7 +1,7 @@
1
1
  import { charge as charge_ } from './Charge.js'
2
2
 
3
3
  /**
4
- * Creates a Stripe `charge` client method intent.
4
+ * Creates a Stripe `charge` client method.
5
5
  *
6
6
  * @example
7
7
  * ```ts
@@ -32,6 +32,6 @@ export function stripe(parameters: stripe.Parameters) {
32
32
  export namespace stripe {
33
33
  export type Parameters = charge_.Parameters
34
34
 
35
- /** Creates a Stripe `charge` client method intent for SPT-based payments. */
35
+ /** Creates a Stripe `charge` client method for SPT-based payments. */
36
36
  export const charge = charge_
37
37
  }
@@ -1,2 +1,2 @@
1
1
  export { charge } from './Charge.js'
2
- export { stripe } from './MethodIntents.js'
2
+ export { stripe } from './Methods.js'
@@ -1 +1 @@
1
- export * as MethodIntents from './Intents.js'
1
+ export * as Methods from './Methods.js'
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Duck-typed interface for the Stripe Node SDK (`stripe` npm package).
3
+ * Matches the subset of the API used by mppx for server-side payment verification.
4
+ *
5
+ * Uses loose signatures so any Stripe SDK version is assignable.
6
+ */
7
+ export type StripeClient = {
8
+ paymentIntents: {
9
+ create(...args: any[]): Promise<{ id: string; status: string }>
10
+ }
11
+ }
12
+
13
+ /**
14
+ * Duck-typed interface for Stripe.js (`@stripe/stripe-js`).
15
+ * Matches the subset of the API used by mppx for client-side payment method creation.
16
+ *
17
+ * Uses loose signatures so any Stripe.js version is assignable.
18
+ */
19
+ export type StripeJs = {
20
+ createPaymentMethod(...args: any[]): Promise<Record<string, unknown>>
21
+ elements(...args: any[]): unknown
22
+ }
@@ -0,0 +1,241 @@
1
+ import { Challenge, Credential } from 'mppx'
2
+ import { Mppx, stripe } from 'mppx/server'
3
+ import { afterEach, describe, expect, test, vi } from 'vitest'
4
+ import * as Http from '~test/Http.js'
5
+ import type { StripeClient } from '../internal/types.js'
6
+
7
+ const realm = 'api.example.com'
8
+ const secretKey = 'test-secret-key'
9
+
10
+ let httpServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
11
+
12
+ afterEach(() => httpServer?.close())
13
+
14
+ function createMockStripeClient(
15
+ overrides?: Partial<{ status: string; id: string; throws: boolean }>,
16
+ ): { client: StripeClient; create: ReturnType<typeof vi.fn> } {
17
+ const { status = 'succeeded', id = 'pi_mock_123', throws = false } = overrides ?? {}
18
+ const create = vi.fn(async () => {
19
+ if (throws) throw new Error('Stripe API error')
20
+ return { id, status }
21
+ })
22
+ return {
23
+ client: { paymentIntents: { create } },
24
+ create,
25
+ }
26
+ }
27
+
28
+ describe('stripe.charge with client', () => {
29
+ test('default: verifies payment via client.paymentIntents.create', async () => {
30
+ const { client, create } = createMockStripeClient()
31
+
32
+ const server = Mppx.create({
33
+ methods: [
34
+ stripe.charge({
35
+ client,
36
+ networkId: 'internal',
37
+ paymentMethodTypes: ['card'],
38
+ }),
39
+ ],
40
+ realm,
41
+ secretKey,
42
+ })
43
+
44
+ httpServer = await Http.createServer(async (req, res) => {
45
+ const result = await Mppx.toNodeListener(
46
+ server.charge({ amount: '1', currency: 'usd', decimals: 2 }),
47
+ )(req, res)
48
+ if (result.status === 402) return
49
+ res.end('OK')
50
+ })
51
+
52
+ const response = await fetch(httpServer.url)
53
+ expect(response.status).toBe(402)
54
+
55
+ const challenge = Challenge.fromResponse(response)
56
+ const credential = Credential.from({
57
+ challenge,
58
+ payload: { spt: 'spt_test_token' },
59
+ })
60
+
61
+ const paidResponse = await fetch(httpServer.url, {
62
+ headers: { Authorization: Credential.serialize(credential) },
63
+ })
64
+ expect(paidResponse.status).toBe(200)
65
+ expect(create).toHaveBeenCalledOnce()
66
+
67
+ const [params, options] = create.mock.calls[0]!
68
+ expect(params).toMatchObject({
69
+ amount: 100,
70
+ confirm: true,
71
+ currency: 'usd',
72
+ payment_method: 'spt_test_token',
73
+ })
74
+ expect(params.automatic_payment_methods).toMatchObject({
75
+ allow_redirects: 'never',
76
+ enabled: true,
77
+ })
78
+ expect(options.idempotencyKey).toMatch(/^mppx_/)
79
+ })
80
+
81
+ test('behavior: includes metadata in client call', async () => {
82
+ const { client, create } = createMockStripeClient()
83
+
84
+ const server = Mppx.create({
85
+ methods: [
86
+ stripe.charge({
87
+ client,
88
+ metadata: { plan: 'pro' },
89
+ networkId: 'internal',
90
+ paymentMethodTypes: ['card'],
91
+ }),
92
+ ],
93
+ realm,
94
+ secretKey,
95
+ })
96
+
97
+ httpServer = await Http.createServer(async (req, res) => {
98
+ const result = await Mppx.toNodeListener(
99
+ server.charge({ amount: '1', currency: 'usd', decimals: 2 }),
100
+ )(req, res)
101
+ if (result.status === 402) return
102
+ res.end('OK')
103
+ })
104
+
105
+ const response = await fetch(httpServer.url)
106
+ const challenge = Challenge.fromResponse(response)
107
+ const credential = Credential.from({
108
+ challenge,
109
+ payload: { spt: 'spt_test_token' },
110
+ })
111
+
112
+ await fetch(httpServer.url, {
113
+ headers: { Authorization: Credential.serialize(credential) },
114
+ })
115
+
116
+ const [params] = create.mock.calls[0]!
117
+ expect(params.metadata).toMatchObject({ plan: 'pro' })
118
+ expect(params.metadata.mpp_is_mpp).toBe('true')
119
+ })
120
+
121
+ test('behavior: rejects when client throws', async () => {
122
+ const { client } = createMockStripeClient({ throws: true })
123
+
124
+ const server = Mppx.create({
125
+ methods: [
126
+ stripe.charge({
127
+ client,
128
+ networkId: 'internal',
129
+ paymentMethodTypes: ['card'],
130
+ }),
131
+ ],
132
+ realm,
133
+ secretKey,
134
+ })
135
+
136
+ httpServer = await Http.createServer(async (req, res) => {
137
+ const result = await Mppx.toNodeListener(
138
+ server.charge({ amount: '1', currency: 'usd', decimals: 2 }),
139
+ )(req, res)
140
+ if (result.status === 402) return
141
+ res.end('OK')
142
+ })
143
+
144
+ const response = await fetch(httpServer.url)
145
+ const challenge = Challenge.fromResponse(response)
146
+ const credential = Credential.from({
147
+ challenge,
148
+ payload: { spt: 'spt_test_token' },
149
+ })
150
+
151
+ const paidResponse = await fetch(httpServer.url, {
152
+ headers: { Authorization: Credential.serialize(credential) },
153
+ })
154
+ expect(paidResponse.status).toBe(402)
155
+ const body = (await paidResponse.json()) as { detail: string }
156
+ expect(body.detail).toContain('Stripe PaymentIntent failed')
157
+ })
158
+
159
+ test('behavior: rejects requires_action status', async () => {
160
+ const { client } = createMockStripeClient({ status: 'requires_action' })
161
+
162
+ const server = Mppx.create({
163
+ methods: [
164
+ stripe.charge({
165
+ client,
166
+ networkId: 'internal',
167
+ paymentMethodTypes: ['card'],
168
+ }),
169
+ ],
170
+ realm,
171
+ secretKey,
172
+ })
173
+
174
+ httpServer = await Http.createServer(async (req, res) => {
175
+ const result = await Mppx.toNodeListener(
176
+ server.charge({ amount: '1', currency: 'usd', decimals: 2 }),
177
+ )(req, res)
178
+ if (result.status === 402) return
179
+ res.end('OK')
180
+ })
181
+
182
+ const response = await fetch(httpServer.url)
183
+ const challenge = Challenge.fromResponse(response)
184
+ const credential = Credential.from({
185
+ challenge,
186
+ payload: { spt: 'spt_test_token' },
187
+ })
188
+
189
+ const paidResponse = await fetch(httpServer.url, {
190
+ headers: { Authorization: Credential.serialize(credential) },
191
+ })
192
+ expect(paidResponse.status).toBe(402)
193
+ const body = (await paidResponse.json()) as { detail: string }
194
+ expect(body.detail).toContain('requires action')
195
+ })
196
+
197
+ test('behavior: receipt contains mock reference', async () => {
198
+ const { client } = createMockStripeClient({ id: 'pi_custom_ref' })
199
+
200
+ const server = Mppx.create({
201
+ methods: [
202
+ stripe.charge({
203
+ client,
204
+ networkId: 'internal',
205
+ paymentMethodTypes: ['card'],
206
+ }),
207
+ ],
208
+ realm,
209
+ secretKey,
210
+ })
211
+
212
+ const handle = server.charge({ amount: '1', currency: 'usd', decimals: 2 })
213
+
214
+ const firstResult = await handle(new Request('https://example.com'))
215
+ expect(firstResult.status).toBe(402)
216
+ if (firstResult.status !== 402) throw new Error()
217
+
218
+ const challenge = Challenge.fromResponse(firstResult.challenge)
219
+ const credential = Credential.from({
220
+ challenge,
221
+ payload: { spt: 'spt_test_token' },
222
+ })
223
+
224
+ const result = await handle(
225
+ new Request('https://example.com', {
226
+ headers: { Authorization: Credential.serialize(credential) },
227
+ }),
228
+ )
229
+ expect(result.status).toBe(200)
230
+ if (result.status !== 200) throw new Error()
231
+
232
+ const wrapped = result.withReceipt(Response.json({ ok: true }))
233
+ const receiptHeader = wrapped.headers.get('Payment-Receipt')
234
+ expect(receiptHeader).toBeTruthy()
235
+
236
+ const decoded = JSON.parse(
237
+ Buffer.from(receiptHeader!.replace('Payment ', ''), 'base64url').toString(),
238
+ ) as { reference: string }
239
+ expect(decoded.reference).toBe('pi_custom_ref')
240
+ })
241
+ })