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.
- package/dist/Challenge.d.ts +16 -16
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +7 -7
- 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 +7 -7
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +3 -3
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +10 -10
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +2 -2
- 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 +40 -27
- package/dist/stripe/client/Charge.d.ts.map +1 -1
- package/dist/stripe/client/Charge.js +15 -7
- package/dist/stripe/client/Charge.js.map +1 -1
- package/dist/stripe/client/{MethodIntents.d.ts → Methods.d.ts} +24 -23
- package/dist/stripe/client/Methods.d.ts.map +1 -0
- package/dist/stripe/client/{MethodIntents.js → Methods.js} +3 -3
- 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.js +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/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/package.json +1 -1
- package/src/Challenge.test-d.ts +3 -3
- package/src/Challenge.test.ts +6 -6
- package/src/Challenge.ts +32 -32
- 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 +10 -10
- package/src/client/Transport.test.ts +2 -2
- package/src/client/internal/Fetch.ts +21 -24
- 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/{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 +29 -16
- package/src/stripe/client/{MethodIntents.ts → Methods.ts} +2 -2
- 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 +1 -1
- package/src/tempo/client/index.ts +1 -1
- package/src/tempo/index.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/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,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
|
|
3
|
+
import * as Method from '../../Method.js'
|
|
4
4
|
import * as z from '../../zod.js'
|
|
5
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
+
})
|