mppx 0.1.0 → 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 (231) hide show
  1. package/README.md +1 -1
  2. package/dist/Challenge.d.ts +18 -18
  3. package/dist/Challenge.d.ts.map +1 -1
  4. package/dist/Challenge.js +8 -8
  5. package/dist/Challenge.js.map +1 -1
  6. package/dist/Errors.d.ts +58 -8
  7. package/dist/Errors.d.ts.map +1 -1
  8. package/dist/Errors.js +51 -9
  9. package/dist/Errors.js.map +1 -1
  10. package/dist/Method.d.ts +154 -0
  11. package/dist/Method.d.ts.map +1 -0
  12. package/dist/Method.js +81 -0
  13. package/dist/Method.js.map +1 -0
  14. package/dist/PaymentRequest.d.ts +5 -5
  15. package/dist/PaymentRequest.d.ts.map +1 -1
  16. package/dist/PaymentRequest.js +5 -5
  17. package/dist/cli.js +67 -18
  18. package/dist/cli.js.map +1 -1
  19. package/dist/client/Methods.d.ts +2 -2
  20. package/dist/client/Methods.d.ts.map +1 -1
  21. package/dist/client/Methods.js +2 -2
  22. package/dist/client/Methods.js.map +1 -1
  23. package/dist/client/Mppx.d.ts +12 -7
  24. package/dist/client/Mppx.d.ts.map +1 -1
  25. package/dist/client/Mppx.js +10 -5
  26. package/dist/client/Mppx.js.map +1 -1
  27. package/dist/client/internal/Fetch.d.ts +13 -11
  28. package/dist/client/internal/Fetch.d.ts.map +1 -1
  29. package/dist/client/internal/Fetch.js +8 -4
  30. package/dist/client/internal/Fetch.js.map +1 -1
  31. package/dist/index.d.ts +1 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/mcp-sdk/client/McpClient.d.ts +6 -6
  36. package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
  37. package/dist/mcp-sdk/client/McpClient.js +4 -4
  38. package/dist/mcp-sdk/client/McpClient.js.map +1 -1
  39. package/dist/middlewares/elysia.d.ts +1 -1
  40. package/dist/middlewares/express.d.ts +1 -1
  41. package/dist/middlewares/hono.d.ts +1 -1
  42. package/dist/middlewares/internal/mppx.d.ts +7 -7
  43. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  44. package/dist/middlewares/internal/mppx.js +5 -5
  45. package/dist/middlewares/internal/mppx.js.map +1 -1
  46. package/dist/middlewares/nextjs.d.ts +1 -1
  47. package/dist/proxy/Service.js +2 -2
  48. package/dist/proxy/Service.js.map +1 -1
  49. package/dist/server/Methods.d.ts +2 -2
  50. package/dist/server/Methods.d.ts.map +1 -1
  51. package/dist/server/Methods.js +2 -2
  52. package/dist/server/Methods.js.map +1 -1
  53. package/dist/server/Mppx.d.ts +17 -17
  54. package/dist/server/Mppx.d.ts.map +1 -1
  55. package/dist/server/Mppx.js +9 -9
  56. package/dist/server/Mppx.js.map +1 -1
  57. package/dist/stripe/{Intents.d.ts → Methods.d.ts} +22 -22
  58. package/dist/stripe/Methods.d.ts.map +1 -0
  59. package/dist/stripe/Methods.js +42 -0
  60. package/dist/stripe/Methods.js.map +1 -0
  61. package/dist/stripe/client/Charge.d.ts +48 -44
  62. package/dist/stripe/client/Charge.d.ts.map +1 -1
  63. package/dist/stripe/client/Charge.js +22 -17
  64. package/dist/stripe/client/Charge.js.map +1 -1
  65. package/dist/stripe/client/{MethodIntents.d.ts → Methods.d.ts} +25 -24
  66. package/dist/stripe/client/Methods.d.ts.map +1 -0
  67. package/dist/stripe/client/{MethodIntents.js → Methods.js} +4 -4
  68. package/dist/stripe/client/Methods.js.map +1 -0
  69. package/dist/stripe/client/index.d.ts +1 -1
  70. package/dist/stripe/client/index.d.ts.map +1 -1
  71. package/dist/stripe/client/index.js +1 -1
  72. package/dist/stripe/client/index.js.map +1 -1
  73. package/dist/stripe/index.d.ts +1 -1
  74. package/dist/stripe/index.d.ts.map +1 -1
  75. package/dist/stripe/index.js +1 -1
  76. package/dist/stripe/index.js.map +1 -1
  77. package/dist/stripe/internal/types.d.ts +25 -0
  78. package/dist/stripe/internal/types.d.ts.map +1 -0
  79. package/dist/stripe/internal/types.js +2 -0
  80. package/dist/stripe/internal/types.js.map +1 -0
  81. package/dist/stripe/server/Charge.d.ts +47 -28
  82. package/dist/stripe/server/Charge.d.ts.map +1 -1
  83. package/dist/stripe/server/Charge.js +90 -32
  84. package/dist/stripe/server/Charge.js.map +1 -1
  85. package/dist/stripe/server/{MethodIntents.d.ts → Methods.d.ts} +24 -23
  86. package/dist/stripe/server/Methods.d.ts.map +1 -0
  87. package/dist/stripe/server/{MethodIntents.js → Methods.js} +3 -3
  88. package/dist/stripe/server/Methods.js.map +1 -0
  89. package/dist/stripe/server/index.d.ts +1 -1
  90. package/dist/stripe/server/index.d.ts.map +1 -1
  91. package/dist/stripe/server/index.js +1 -1
  92. package/dist/stripe/server/index.js.map +1 -1
  93. package/dist/tempo/{Intents.d.ts → Methods.d.ts} +72 -69
  94. package/dist/tempo/Methods.d.ts.map +1 -0
  95. package/dist/tempo/Methods.js +118 -0
  96. package/dist/tempo/Methods.js.map +1 -0
  97. package/dist/tempo/client/ChannelOps.d.ts +1 -1
  98. package/dist/tempo/client/ChannelOps.js +1 -1
  99. package/dist/tempo/client/Charge.d.ts +25 -25
  100. package/dist/tempo/client/Charge.d.ts.map +1 -1
  101. package/dist/tempo/client/Charge.js +3 -3
  102. package/dist/tempo/client/Charge.js.map +1 -1
  103. package/dist/tempo/client/{MethodIntents.d.ts → Methods.d.ts} +74 -70
  104. package/dist/tempo/client/Methods.d.ts.map +1 -0
  105. package/dist/tempo/client/{MethodIntents.js → Methods.js} +3 -3
  106. package/dist/tempo/client/Methods.js.map +1 -0
  107. package/dist/tempo/client/Session.d.ts +49 -45
  108. package/dist/tempo/client/Session.d.ts.map +1 -1
  109. package/dist/tempo/client/Session.js +4 -4
  110. package/dist/tempo/client/Session.js.map +1 -1
  111. package/dist/tempo/client/SessionManager.d.ts +1 -1
  112. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  113. package/dist/tempo/client/SessionManager.js +10 -5
  114. package/dist/tempo/client/SessionManager.js.map +1 -1
  115. package/dist/tempo/client/index.d.ts +1 -1
  116. package/dist/tempo/client/index.d.ts.map +1 -1
  117. package/dist/tempo/client/index.js +1 -1
  118. package/dist/tempo/client/index.js.map +1 -1
  119. package/dist/tempo/index.d.ts +1 -1
  120. package/dist/tempo/index.d.ts.map +1 -1
  121. package/dist/tempo/index.js +1 -1
  122. package/dist/tempo/index.js.map +1 -1
  123. package/dist/tempo/internal/defaults.d.ts +1 -1
  124. package/dist/tempo/internal/defaults.js +1 -1
  125. package/dist/tempo/server/Charge.d.ts +27 -27
  126. package/dist/tempo/server/Charge.d.ts.map +1 -1
  127. package/dist/tempo/server/Charge.js +3 -3
  128. package/dist/tempo/server/Charge.js.map +1 -1
  129. package/dist/tempo/server/{MethodIntents.d.ts → Methods.d.ts} +73 -69
  130. package/dist/tempo/server/Methods.d.ts.map +1 -0
  131. package/dist/tempo/server/{MethodIntents.js → Methods.js} +4 -4
  132. package/dist/tempo/server/Methods.js.map +1 -0
  133. package/dist/tempo/server/Session.d.ts +51 -47
  134. package/dist/tempo/server/Session.d.ts.map +1 -1
  135. package/dist/tempo/server/Session.js +4 -4
  136. package/dist/tempo/server/Session.js.map +1 -1
  137. package/dist/tempo/server/index.d.ts +6 -0
  138. package/dist/tempo/server/index.d.ts.map +1 -0
  139. package/dist/tempo/server/index.js +6 -0
  140. package/dist/tempo/server/index.js.map +1 -0
  141. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  142. package/dist/tempo/server/internal/transport.js +2 -1
  143. package/dist/tempo/server/internal/transport.js.map +1 -1
  144. package/package.json +1 -1
  145. package/src/Challenge.test-d.ts +3 -3
  146. package/src/Challenge.test.ts +6 -6
  147. package/src/Challenge.ts +34 -34
  148. package/src/Errors.test.ts +75 -21
  149. package/src/Errors.ts +74 -9
  150. package/src/Method.test.ts +76 -0
  151. package/src/Method.ts +228 -0
  152. package/src/PaymentRequest.test.ts +4 -4
  153. package/src/PaymentRequest.ts +9 -9
  154. package/src/cli.test.ts +12 -22
  155. package/src/cli.ts +74 -21
  156. package/src/client/Methods.ts +2 -2
  157. package/src/client/Mppx.test-d.ts +6 -6
  158. package/src/client/Mppx.test.ts +26 -22
  159. package/src/client/Mppx.ts +29 -13
  160. package/src/client/Transport.test.ts +2 -2
  161. package/src/client/internal/Fetch.test.ts +35 -1
  162. package/src/client/internal/Fetch.ts +36 -27
  163. package/src/index.ts +1 -2
  164. package/src/mcp-sdk/client/McpClient.ts +11 -13
  165. package/src/middlewares/elysia.ts +1 -1
  166. package/src/middlewares/express.ts +1 -1
  167. package/src/middlewares/hono.ts +1 -1
  168. package/src/middlewares/internal/mppx.ts +10 -10
  169. package/src/middlewares/nextjs.ts +1 -1
  170. package/src/proxy/Service.ts +2 -2
  171. package/src/server/Methods.ts +2 -2
  172. package/src/server/Mppx.test-d.ts +27 -29
  173. package/src/server/Mppx.test.ts +23 -19
  174. package/src/server/Mppx.ts +43 -43
  175. package/src/server/Transport.test.ts +3 -3
  176. package/src/stripe/Charge.integration.test.ts +4 -1
  177. package/src/stripe/{Intents.test.ts → Methods.test.ts} +12 -12
  178. package/src/stripe/Methods.ts +45 -0
  179. package/src/stripe/client/Charge.test.ts +189 -0
  180. package/src/stripe/client/Charge.ts +40 -31
  181. package/src/stripe/client/{MethodIntents.ts → Methods.ts} +3 -3
  182. package/src/stripe/client/index.ts +1 -1
  183. package/src/stripe/index.ts +1 -1
  184. package/src/stripe/internal/types.ts +22 -0
  185. package/src/stripe/server/Charge.test.ts +241 -0
  186. package/src/stripe/server/Charge.ts +124 -38
  187. package/src/stripe/server/{MethodIntents.ts → Methods.ts} +2 -2
  188. package/src/stripe/server/index.ts +1 -1
  189. package/src/tempo/{Intents.test.ts → Methods.test.ts} +15 -15
  190. package/src/tempo/{Intents.ts → Methods.ts} +77 -22
  191. package/src/tempo/client/ChannelOps.ts +1 -1
  192. package/src/tempo/client/Charge.ts +3 -3
  193. package/src/tempo/client/{MethodIntents.ts → Methods.ts} +2 -2
  194. package/src/tempo/client/Session.ts +4 -4
  195. package/src/tempo/client/SessionManager.ts +11 -5
  196. package/src/tempo/client/index.ts +1 -1
  197. package/src/tempo/index.ts +1 -1
  198. package/src/tempo/internal/defaults.ts +1 -1
  199. package/src/tempo/server/Charge.ts +4 -7
  200. package/src/tempo/server/{MethodIntents.ts → Methods.ts} +3 -3
  201. package/src/tempo/server/Session.test.ts +4 -7
  202. package/src/tempo/server/Session.ts +6 -6
  203. package/src/tempo/server/index.ts +1 -1
  204. package/src/tempo/server/internal/transport.ts +3 -2
  205. package/dist/Intent.d.ts +0 -101
  206. package/dist/Intent.d.ts.map +0 -1
  207. package/dist/Intent.js +0 -83
  208. package/dist/Intent.js.map +0 -1
  209. package/dist/MethodIntent.d.ts +0 -225
  210. package/dist/MethodIntent.d.ts.map +0 -1
  211. package/dist/MethodIntent.js +0 -156
  212. package/dist/MethodIntent.js.map +0 -1
  213. package/dist/stripe/Intents.d.ts.map +0 -1
  214. package/dist/stripe/Intents.js +0 -27
  215. package/dist/stripe/Intents.js.map +0 -1
  216. package/dist/stripe/client/MethodIntents.d.ts.map +0 -1
  217. package/dist/stripe/client/MethodIntents.js.map +0 -1
  218. package/dist/stripe/server/MethodIntents.d.ts.map +0 -1
  219. package/dist/stripe/server/MethodIntents.js.map +0 -1
  220. package/dist/tempo/Intents.d.ts.map +0 -1
  221. package/dist/tempo/Intents.js +0 -81
  222. package/dist/tempo/Intents.js.map +0 -1
  223. package/dist/tempo/client/MethodIntents.d.ts.map +0 -1
  224. package/dist/tempo/client/MethodIntents.js.map +0 -1
  225. package/dist/tempo/server/MethodIntents.d.ts.map +0 -1
  226. package/dist/tempo/server/MethodIntents.js.map +0 -1
  227. package/src/Intent.test.ts +0 -180
  228. package/src/Intent.ts +0 -109
  229. package/src/MethodIntent.test.ts +0 -303
  230. package/src/MethodIntent.ts +0 -388
  231. package/src/stripe/Intents.ts +0 -27
@@ -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,23 +1,33 @@
1
+ import type * as Challenge from '../../Challenge.js'
1
2
  import * as Credential from '../../Credential.js'
2
- import * as MethodIntent from '../../MethodIntent.js'
3
+ import * as Method from '../../Method.js'
3
4
  import * as z from '../../zod.js'
4
- import * as Intents from '../Intents.js'
5
+ import type { StripeJs } from '../internal/types.js'
6
+ import * as Methods from '../Methods.js'
5
7
 
6
8
  /**
7
9
  * Creates a Stripe charge method intent for usage on the client.
8
10
  *
9
- * Accepts a `createSpt` callback that handles SPT creation (requires
10
- * a secret key, so typically proxied through a server endpoint).
11
+ * Accepts a `createToken` callback that handles SPT creation (requires
12
+ * a secret key, so typically proxied through a server endpoint) and
13
+ * returns a credential for retrying the request.
11
14
  *
12
15
  * The `paymentMethod` (e.g. from Stripe Elements) can be provided at
13
16
  * initialization or at credential-creation time via `context`.
14
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
+ *
15
21
  * @example
16
22
  * ```ts
23
+ * import { loadStripe } from '@stripe/stripe-js'
17
24
  * import { stripe } from 'mppx/client'
18
25
  *
26
+ * const stripeJs = await loadStripe('pk_...')
27
+ *
19
28
  * const charge = stripe.charge({
20
- * createSpt: async ({ paymentMethod, amount, currency, networkId, expiresAt, metadata }) => {
29
+ * client: stripeJs,
30
+ * createToken: async ({ amount, currency, expiresAt, metadata, networkId, paymentMethod }) => {
21
31
  * const res = await fetch('/api/create-spt', {
22
32
  * method: 'POST',
23
33
  * headers: { 'Content-Type': 'application/json' },
@@ -27,26 +37,21 @@ import * as Intents from '../Intents.js'
27
37
  * return spt
28
38
  * },
29
39
  * })
30
- *
31
- * // paymentMethod comes from Stripe Elements at credential-creation time
32
- * const credential = await charge.createCredential({
33
- * challenge,
34
- * context: { paymentMethod: 'pm_xxx' },
35
- * })
36
40
  * ```
37
41
  */
38
42
  export function charge(parameters: charge.Parameters) {
39
- const { createSpt, externalId, paymentMethod: defaultPaymentMethod } = parameters
43
+ const { client, createToken, externalId, paymentMethod: defaultPaymentMethod } = parameters
40
44
 
41
- return MethodIntent.toClient(Intents.charge, {
45
+ return Method.toClient(Methods.charge, {
42
46
  context: z.object({
43
47
  paymentMethod: z.optional(z.string()),
44
48
  }),
45
49
 
46
50
  async createCredential({ challenge, context }) {
47
51
  const paymentMethod = context?.paymentMethod ?? defaultPaymentMethod
48
- if (!paymentMethod)
52
+ if (!paymentMethod) {
49
53
  throw new Error('paymentMethod is required (pass via context or parameters)')
54
+ }
50
55
 
51
56
  const amount = challenge.request.amount as string
52
57
  const currency = challenge.request.currency as string
@@ -65,13 +70,15 @@ export function charge(parameters: charge.Parameters) {
65
70
  ? Math.floor(new Date(challenge.request.expires as string).getTime() / 1000)
66
71
  : Math.floor(Date.now() / 1000) + 3600
67
72
 
68
- const spt = await createSpt({
69
- paymentMethod,
73
+ const spt = await createToken({
70
74
  amount,
75
+ challenge,
76
+ client,
71
77
  currency,
72
- networkId,
73
78
  expiresAt,
74
79
  metadata,
80
+ networkId,
81
+ paymentMethod,
75
82
  })
76
83
 
77
84
  return Credential.serialize({
@@ -87,33 +94,35 @@ export function charge(parameters: charge.Parameters) {
87
94
 
88
95
  export declare namespace charge {
89
96
  type Parameters = {
90
- /**
91
- * Creates a Shared Payment Token (SPT) for the given parameters.
92
- *
93
- * SPT creation requires a Stripe secret key, so this typically
94
- * proxies through a server endpoint (e.g. `POST /api/create-spt`).
95
- * If you are running a client in an enviroment with a secret key, you can just create the
96
- * SPT directly in this callback.
97
- */
98
- createSpt: (parameters: CreateSptParameters) => Promise<string>
97
+ /** Stripe.js instance from `@stripe/stripe-js`. Forwarded to `createToken` for use with Elements. */
98
+ client?: StripeJs | undefined
99
+ /** Called when a Stripe challenge is received. Create an SPT to retry. */
100
+ createToken: (parameters: OnChallengeParameters) => Promise<string>
99
101
  /** Optional client-side external reference ID for the credential payload. */
100
102
  externalId?: string | undefined
101
103
  /** Default payment method ID. Overridden by `context.paymentMethod`. */
102
104
  paymentMethod?: string | undefined
103
105
  }
104
106
 
105
- type CreateSptParameters = {
106
- /** Stripe payment method ID (e.g. from Stripe Elements). */
107
- paymentMethod: string
107
+ type OnChallengeParameters = {
108
108
  /** Payment amount (in smallest currency unit). */
109
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
110
117
  /** Three-letter ISO currency code. */
111
118
  currency: string
112
- /** Stripe Business Network profile ID. */
113
- networkId: string | undefined
114
119
  /** SPT expiration as a Unix timestamp (seconds). */
115
120
  expiresAt: number
116
121
  /** Optional metadata to associate with the SPT. */
117
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
118
127
  }
119
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
@@ -10,7 +10,7 @@ import { charge as charge_ } from './Charge.js'
10
10
  * const mppx = Mppx.create({
11
11
  * methods: [
12
12
  * stripe({
13
- * createSpt: async (params) => {
13
+ * createToken: async (params) => {
14
14
  * const res = await fetch('/api/create-spt', {
15
15
  * method: 'POST',
16
16
  * headers: { 'Content-Type': 'application/json' },
@@ -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
+ })