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.
- package/README.md +1 -1
- package/dist/Challenge.d.ts +18 -18
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +8 -8
- package/dist/Challenge.js.map +1 -1
- package/dist/Errors.d.ts +58 -8
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +51 -9
- package/dist/Errors.js.map +1 -1
- package/dist/Method.d.ts +154 -0
- package/dist/Method.d.ts.map +1 -0
- package/dist/Method.js +81 -0
- package/dist/Method.js.map +1 -0
- package/dist/PaymentRequest.d.ts +5 -5
- package/dist/PaymentRequest.d.ts.map +1 -1
- package/dist/PaymentRequest.js +5 -5
- package/dist/cli.js +67 -18
- package/dist/cli.js.map +1 -1
- package/dist/client/Methods.d.ts +2 -2
- package/dist/client/Methods.d.ts.map +1 -1
- package/dist/client/Methods.js +2 -2
- package/dist/client/Methods.js.map +1 -1
- package/dist/client/Mppx.d.ts +12 -7
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +10 -5
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +13 -11
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +8 -4
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp-sdk/client/McpClient.d.ts +6 -6
- package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.js +4 -4
- package/dist/mcp-sdk/client/McpClient.js.map +1 -1
- package/dist/middlewares/elysia.d.ts +1 -1
- package/dist/middlewares/express.d.ts +1 -1
- package/dist/middlewares/hono.d.ts +1 -1
- package/dist/middlewares/internal/mppx.d.ts +7 -7
- package/dist/middlewares/internal/mppx.d.ts.map +1 -1
- package/dist/middlewares/internal/mppx.js +5 -5
- package/dist/middlewares/internal/mppx.js.map +1 -1
- package/dist/middlewares/nextjs.d.ts +1 -1
- package/dist/proxy/Service.js +2 -2
- package/dist/proxy/Service.js.map +1 -1
- package/dist/server/Methods.d.ts +2 -2
- package/dist/server/Methods.d.ts.map +1 -1
- package/dist/server/Methods.js +2 -2
- package/dist/server/Methods.js.map +1 -1
- package/dist/server/Mppx.d.ts +17 -17
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +9 -9
- package/dist/server/Mppx.js.map +1 -1
- package/dist/stripe/{Intents.d.ts → Methods.d.ts} +22 -22
- package/dist/stripe/Methods.d.ts.map +1 -0
- package/dist/stripe/Methods.js +42 -0
- package/dist/stripe/Methods.js.map +1 -0
- package/dist/stripe/client/Charge.d.ts +48 -44
- package/dist/stripe/client/Charge.d.ts.map +1 -1
- package/dist/stripe/client/Charge.js +22 -17
- package/dist/stripe/client/Charge.js.map +1 -1
- package/dist/stripe/client/{MethodIntents.d.ts → Methods.d.ts} +25 -24
- package/dist/stripe/client/Methods.d.ts.map +1 -0
- package/dist/stripe/client/{MethodIntents.js → Methods.js} +4 -4
- package/dist/stripe/client/Methods.js.map +1 -0
- package/dist/stripe/client/index.d.ts +1 -1
- package/dist/stripe/client/index.d.ts.map +1 -1
- package/dist/stripe/client/index.js +1 -1
- package/dist/stripe/client/index.js.map +1 -1
- package/dist/stripe/index.d.ts +1 -1
- package/dist/stripe/index.d.ts.map +1 -1
- package/dist/stripe/index.js +1 -1
- package/dist/stripe/index.js.map +1 -1
- package/dist/stripe/internal/types.d.ts +25 -0
- package/dist/stripe/internal/types.d.ts.map +1 -0
- package/dist/stripe/internal/types.js +2 -0
- package/dist/stripe/internal/types.js.map +1 -0
- package/dist/stripe/server/Charge.d.ts +47 -28
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +90 -32
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/{MethodIntents.d.ts → Methods.d.ts} +24 -23
- package/dist/stripe/server/Methods.d.ts.map +1 -0
- package/dist/stripe/server/{MethodIntents.js → Methods.js} +3 -3
- package/dist/stripe/server/Methods.js.map +1 -0
- package/dist/stripe/server/index.d.ts +1 -1
- package/dist/stripe/server/index.d.ts.map +1 -1
- package/dist/stripe/server/index.js +1 -1
- package/dist/stripe/server/index.js.map +1 -1
- package/dist/tempo/{Intents.d.ts → Methods.d.ts} +72 -69
- package/dist/tempo/Methods.d.ts.map +1 -0
- package/dist/tempo/Methods.js +118 -0
- package/dist/tempo/Methods.js.map +1 -0
- package/dist/tempo/client/ChannelOps.d.ts +1 -1
- package/dist/tempo/client/ChannelOps.js +1 -1
- package/dist/tempo/client/Charge.d.ts +25 -25
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +3 -3
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/{MethodIntents.d.ts → Methods.d.ts} +74 -70
- package/dist/tempo/client/Methods.d.ts.map +1 -0
- package/dist/tempo/client/{MethodIntents.js → Methods.js} +3 -3
- package/dist/tempo/client/Methods.js.map +1 -0
- package/dist/tempo/client/Session.d.ts +49 -45
- package/dist/tempo/client/Session.d.ts.map +1 -1
- package/dist/tempo/client/Session.js +4 -4
- package/dist/tempo/client/Session.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts +1 -1
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +10 -5
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/client/index.d.ts +1 -1
- package/dist/tempo/client/index.d.ts.map +1 -1
- package/dist/tempo/client/index.js +1 -1
- package/dist/tempo/client/index.js.map +1 -1
- package/dist/tempo/index.d.ts +1 -1
- package/dist/tempo/index.d.ts.map +1 -1
- package/dist/tempo/index.js +1 -1
- package/dist/tempo/index.js.map +1 -1
- package/dist/tempo/internal/defaults.d.ts +1 -1
- package/dist/tempo/internal/defaults.js +1 -1
- package/dist/tempo/server/Charge.d.ts +27 -27
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +3 -3
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/{MethodIntents.d.ts → Methods.d.ts} +73 -69
- package/dist/tempo/server/Methods.d.ts.map +1 -0
- package/dist/tempo/server/{MethodIntents.js → Methods.js} +4 -4
- package/dist/tempo/server/Methods.js.map +1 -0
- package/dist/tempo/server/Session.d.ts +51 -47
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +4 -4
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/index.d.ts +6 -0
- package/dist/tempo/server/index.d.ts.map +1 -0
- package/dist/tempo/server/index.js +6 -0
- package/dist/tempo/server/index.js.map +1 -0
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +2 -1
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/package.json +1 -1
- package/src/Challenge.test-d.ts +3 -3
- package/src/Challenge.test.ts +6 -6
- package/src/Challenge.ts +34 -34
- package/src/Errors.test.ts +75 -21
- package/src/Errors.ts +74 -9
- package/src/Method.test.ts +76 -0
- package/src/Method.ts +228 -0
- package/src/PaymentRequest.test.ts +4 -4
- package/src/PaymentRequest.ts +9 -9
- package/src/cli.test.ts +12 -22
- package/src/cli.ts +74 -21
- package/src/client/Methods.ts +2 -2
- package/src/client/Mppx.test-d.ts +6 -6
- package/src/client/Mppx.test.ts +26 -22
- package/src/client/Mppx.ts +29 -13
- package/src/client/Transport.test.ts +2 -2
- package/src/client/internal/Fetch.test.ts +35 -1
- package/src/client/internal/Fetch.ts +36 -27
- package/src/index.ts +1 -2
- package/src/mcp-sdk/client/McpClient.ts +11 -13
- package/src/middlewares/elysia.ts +1 -1
- package/src/middlewares/express.ts +1 -1
- package/src/middlewares/hono.ts +1 -1
- package/src/middlewares/internal/mppx.ts +10 -10
- package/src/middlewares/nextjs.ts +1 -1
- package/src/proxy/Service.ts +2 -2
- package/src/server/Methods.ts +2 -2
- package/src/server/Mppx.test-d.ts +27 -29
- package/src/server/Mppx.test.ts +23 -19
- package/src/server/Mppx.ts +43 -43
- package/src/server/Transport.test.ts +3 -3
- package/src/stripe/Charge.integration.test.ts +4 -1
- package/src/stripe/{Intents.test.ts → Methods.test.ts} +12 -12
- package/src/stripe/Methods.ts +45 -0
- package/src/stripe/client/Charge.test.ts +189 -0
- package/src/stripe/client/Charge.ts +40 -31
- package/src/stripe/client/{MethodIntents.ts → Methods.ts} +3 -3
- package/src/stripe/client/index.ts +1 -1
- package/src/stripe/index.ts +1 -1
- package/src/stripe/internal/types.ts +22 -0
- package/src/stripe/server/Charge.test.ts +241 -0
- package/src/stripe/server/Charge.ts +124 -38
- package/src/stripe/server/{MethodIntents.ts → Methods.ts} +2 -2
- package/src/stripe/server/index.ts +1 -1
- package/src/tempo/{Intents.test.ts → Methods.test.ts} +15 -15
- package/src/tempo/{Intents.ts → Methods.ts} +77 -22
- package/src/tempo/client/ChannelOps.ts +1 -1
- package/src/tempo/client/Charge.ts +3 -3
- package/src/tempo/client/{MethodIntents.ts → Methods.ts} +2 -2
- package/src/tempo/client/Session.ts +4 -4
- package/src/tempo/client/SessionManager.ts +11 -5
- package/src/tempo/client/index.ts +1 -1
- package/src/tempo/index.ts +1 -1
- package/src/tempo/internal/defaults.ts +1 -1
- package/src/tempo/server/Charge.ts +4 -7
- package/src/tempo/server/{MethodIntents.ts → Methods.ts} +3 -3
- package/src/tempo/server/Session.test.ts +4 -7
- package/src/tempo/server/Session.ts +6 -6
- package/src/tempo/server/index.ts +1 -1
- package/src/tempo/server/internal/transport.ts +3 -2
- package/dist/Intent.d.ts +0 -101
- package/dist/Intent.d.ts.map +0 -1
- package/dist/Intent.js +0 -83
- package/dist/Intent.js.map +0 -1
- package/dist/MethodIntent.d.ts +0 -225
- package/dist/MethodIntent.d.ts.map +0 -1
- package/dist/MethodIntent.js +0 -156
- package/dist/MethodIntent.js.map +0 -1
- package/dist/stripe/Intents.d.ts.map +0 -1
- package/dist/stripe/Intents.js +0 -27
- package/dist/stripe/Intents.js.map +0 -1
- package/dist/stripe/client/MethodIntents.d.ts.map +0 -1
- package/dist/stripe/client/MethodIntents.js.map +0 -1
- package/dist/stripe/server/MethodIntents.d.ts.map +0 -1
- package/dist/stripe/server/MethodIntents.js.map +0 -1
- package/dist/tempo/Intents.d.ts.map +0 -1
- package/dist/tempo/Intents.js +0 -81
- package/dist/tempo/Intents.js.map +0 -1
- package/dist/tempo/client/MethodIntents.d.ts.map +0 -1
- package/dist/tempo/client/MethodIntents.js.map +0 -1
- package/dist/tempo/server/MethodIntents.d.ts.map +0 -1
- package/dist/tempo/server/MethodIntents.js.map +0 -1
- package/src/Intent.test.ts +0 -180
- package/src/Intent.ts +0 -109
- package/src/MethodIntent.test.ts +0 -303
- package/src/MethodIntent.ts +0 -388
- 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
|
|
3
|
+
import * as Method from '../../Method.js'
|
|
3
4
|
import * as z from '../../zod.js'
|
|
4
|
-
import
|
|
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 `
|
|
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
|
-
*
|
|
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 {
|
|
43
|
+
const { client, createToken, externalId, paymentMethod: defaultPaymentMethod } = parameters
|
|
40
44
|
|
|
41
|
-
return
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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 './
|
|
2
|
+
export { stripe } from './Methods.js'
|
package/src/stripe/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * as
|
|
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
|
+
})
|