@deiondz/better-auth-razorpay 2.0.1 → 2.0.2
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 +6 -1
- package/api/index.ts +1 -0
- package/api/verify-payment.ts +87 -0
- package/client/hooks.ts +43 -0
- package/client/types.ts +17 -0
- package/index.ts +6 -0
- package/lib/types.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,7 @@ export const auth = betterAuth({
|
|
|
84
84
|
razorpayPlugin({
|
|
85
85
|
razorpayClient,
|
|
86
86
|
razorpayWebhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
|
|
87
|
+
razorpayKeySecret: process.env.RAZORPAY_KEY_SECRET, // optional: enables verify-payment endpoint
|
|
87
88
|
createCustomerOnSignUp: true, // optional
|
|
88
89
|
subscription: {
|
|
89
90
|
enabled: true,
|
|
@@ -141,6 +142,7 @@ BETTER_AUTH_URL=https://yourdomain.com
|
|
|
141
142
|
RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxx
|
|
142
143
|
RAZORPAY_KEY_SECRET=your_secret_key
|
|
143
144
|
RAZORPAY_WEBHOOK_SECRET=your_webhook_secret
|
|
145
|
+
# Pass RAZORPAY_KEY_SECRET as razorpayKeySecret in plugin options to enable the verify-payment endpoint (same as client key, not webhook secret).
|
|
144
146
|
```
|
|
145
147
|
|
|
146
148
|
5. **Run Database Migration**
|
|
@@ -163,6 +165,7 @@ npx @better-auth/cli@latest generate
|
|
|
163
165
|
interface RazorpayPluginOptions {
|
|
164
166
|
razorpayClient: Razorpay // Required: Initialized Razorpay instance (key_id, key_secret)
|
|
165
167
|
razorpayWebhookSecret?: string // Optional: Webhook secret for signature verification
|
|
168
|
+
razorpayKeySecret?: string // Optional: API key secret for payment signature verification; when set, enables POST /razorpay/verify-payment (same secret as Razorpay client, not webhook secret)
|
|
166
169
|
createCustomerOnSignUp?: boolean // Optional: Create Razorpay customer on user sign-up (default: false)
|
|
167
170
|
onCustomerCreate?: (args) => Promise<void>
|
|
168
171
|
getCustomerCreateParams?: (args) => Promise<{ params?: Record<string, unknown> }>
|
|
@@ -197,6 +200,7 @@ Example: using callbacks in your config:
|
|
|
197
200
|
razorpayPlugin({
|
|
198
201
|
razorpayClient,
|
|
199
202
|
razorpayWebhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
|
|
203
|
+
razorpayKeySecret: process.env.RAZORPAY_KEY_SECRET, // optional: enables verify-payment endpoint
|
|
200
204
|
createCustomerOnSignUp: true,
|
|
201
205
|
onCustomerCreate: async ({ user, razorpayCustomer }) => {
|
|
202
206
|
console.log(`Razorpay customer created for user ${user.id}: ${razorpayCustomer.id}`)
|
|
@@ -429,7 +433,7 @@ if (response.success && response.data) {
|
|
|
429
433
|
|
|
430
434
|
### 4. Verify Payment
|
|
431
435
|
|
|
432
|
-
Verify payment signature after Razorpay checkout completion.
|
|
436
|
+
Verify payment signature after Razorpay checkout completion. This endpoint is **only registered when `razorpayKeySecret`** is set in plugin options. Use the same API key secret as your Razorpay client (not the webhook secret).
|
|
433
437
|
|
|
434
438
|
**Endpoint:** `POST /api/auth/razorpay/verify-payment`
|
|
435
439
|
|
|
@@ -484,6 +488,7 @@ const handlePaymentSuccess = async (razorpayResponse: {
|
|
|
484
488
|
- `SIGNATURE_VERIFICATION_FAILED` - Invalid payment signature
|
|
485
489
|
- `UNAUTHORIZED` - User not authenticated
|
|
486
490
|
- `SUBSCRIPTION_NOT_FOUND` - Subscription record not found
|
|
491
|
+
- `FORBIDDEN` - Subscription does not belong to authenticated user
|
|
487
492
|
|
|
488
493
|
---
|
|
489
494
|
|
package/api/index.ts
CHANGED
|
@@ -3,4 +3,5 @@ export { createOrUpdateSubscription } from './create-or-update-subscription'
|
|
|
3
3
|
export { getPlans } from './get-plans'
|
|
4
4
|
export { listSubscriptions } from './list-subscriptions'
|
|
5
5
|
export { restoreSubscription } from './restore-subscription'
|
|
6
|
+
export { verifyPayment } from './verify-payment'
|
|
6
7
|
export { webhook } from './webhook'
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto'
|
|
2
|
+
import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
|
|
3
|
+
import {
|
|
4
|
+
handleRazorpayError,
|
|
5
|
+
verifyPaymentSchema,
|
|
6
|
+
type SubscriptionRecord,
|
|
7
|
+
} from '../lib'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /api/auth/razorpay/verify-payment
|
|
11
|
+
* Verifies payment signature after Razorpay subscription checkout completion.
|
|
12
|
+
* Requires razorpayKeySecret to be set in plugin options.
|
|
13
|
+
*/
|
|
14
|
+
export const verifyPayment = (keySecret: string) =>
|
|
15
|
+
createAuthEndpoint(
|
|
16
|
+
'/razorpay/verify-payment',
|
|
17
|
+
{ method: 'POST', use: [sessionMiddleware] },
|
|
18
|
+
async (ctx) => {
|
|
19
|
+
try {
|
|
20
|
+
const body = verifyPaymentSchema.parse(ctx.body)
|
|
21
|
+
const { razorpay_payment_id, razorpay_subscription_id, razorpay_signature } = body
|
|
22
|
+
|
|
23
|
+
const generatedSignature = createHmac('sha256', keySecret)
|
|
24
|
+
.update(`${razorpay_payment_id}|${razorpay_subscription_id}`)
|
|
25
|
+
.digest('hex')
|
|
26
|
+
|
|
27
|
+
if (generatedSignature !== razorpay_signature) {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
error: {
|
|
31
|
+
code: 'SIGNATURE_VERIFICATION_FAILED',
|
|
32
|
+
description: 'Payment signature verification failed',
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const userId = ctx.context.session?.user?.id
|
|
38
|
+
if (!userId) {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
error: { code: 'UNAUTHORIZED', description: 'User not authenticated' },
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const record = (await ctx.context.adapter.findOne({
|
|
46
|
+
model: 'subscription',
|
|
47
|
+
where: [{ field: 'razorpaySubscriptionId', value: razorpay_subscription_id }],
|
|
48
|
+
})) as SubscriptionRecord | null
|
|
49
|
+
|
|
50
|
+
if (!record) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
error: { code: 'SUBSCRIPTION_NOT_FOUND', description: 'Subscription not found' },
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (record.referenceId !== userId) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
error: { code: 'FORBIDDEN', description: 'Subscription does not belong to you' },
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await ctx.context.adapter.update({
|
|
65
|
+
model: 'subscription',
|
|
66
|
+
where: [{ field: 'razorpaySubscriptionId', value: razorpay_subscription_id }],
|
|
67
|
+
update: {
|
|
68
|
+
data: {
|
|
69
|
+
status: 'pending',
|
|
70
|
+
updatedAt: new Date(),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
data: {
|
|
78
|
+
message: 'Payment verified successfully',
|
|
79
|
+
payment_id: razorpay_payment_id,
|
|
80
|
+
subscription_id: razorpay_subscription_id,
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return handleRazorpayError(error)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
)
|
package/client/hooks.ts
CHANGED
|
@@ -17,11 +17,13 @@ import type {
|
|
|
17
17
|
CancelSubscriptionInput,
|
|
18
18
|
RestoreSubscriptionInput,
|
|
19
19
|
ListSubscriptionsInput,
|
|
20
|
+
VerifyPaymentInput,
|
|
20
21
|
GetPlansResponse,
|
|
21
22
|
ListSubscriptionsResponse,
|
|
22
23
|
CreateOrUpdateSubscriptionResponse,
|
|
23
24
|
CancelSubscriptionResponse,
|
|
24
25
|
RestoreSubscriptionResponse,
|
|
26
|
+
VerifyPaymentResponse,
|
|
25
27
|
RazorpayApiError,
|
|
26
28
|
} from './types'
|
|
27
29
|
|
|
@@ -103,6 +105,18 @@ async function restoreSubscription(
|
|
|
103
105
|
return res.data
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
/** Verify payment (POST /razorpay/verify-payment). */
|
|
109
|
+
async function verifyPayment(
|
|
110
|
+
client: RazorpayAuthClient,
|
|
111
|
+
input: VerifyPaymentInput
|
|
112
|
+
): Promise<VerifyPaymentResponse['data']> {
|
|
113
|
+
const res = await client.api.post(`${BASE}/verify-payment`, {
|
|
114
|
+
body: input as unknown as Record<string, unknown>,
|
|
115
|
+
})
|
|
116
|
+
assertSuccess<VerifyPaymentResponse['data']>(res)
|
|
117
|
+
return res.data
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
export type UsePlansOptions = Omit<
|
|
107
121
|
UseQueryOptions<PlanSummary[], Error, PlanSummary[], readonly string[]>,
|
|
108
122
|
'queryKey' | 'queryFn'
|
|
@@ -219,11 +233,13 @@ export type {
|
|
|
219
233
|
CancelSubscriptionInput,
|
|
220
234
|
RestoreSubscriptionInput,
|
|
221
235
|
ListSubscriptionsInput,
|
|
236
|
+
VerifyPaymentInput,
|
|
222
237
|
GetPlansResponse,
|
|
223
238
|
ListSubscriptionsResponse,
|
|
224
239
|
CreateOrUpdateSubscriptionResponse,
|
|
225
240
|
CancelSubscriptionResponse,
|
|
226
241
|
RestoreSubscriptionResponse,
|
|
242
|
+
VerifyPaymentResponse,
|
|
227
243
|
RazorpayApiError,
|
|
228
244
|
RazorpayApiResult,
|
|
229
245
|
} from './types'
|
|
@@ -246,3 +262,30 @@ export function useRestoreSubscription(
|
|
|
246
262
|
},
|
|
247
263
|
})
|
|
248
264
|
}
|
|
265
|
+
|
|
266
|
+
export type UseVerifyPaymentOptions = UseMutationOptions<
|
|
267
|
+
VerifyPaymentResponse['data'],
|
|
268
|
+
Error,
|
|
269
|
+
VerifyPaymentInput,
|
|
270
|
+
unknown
|
|
271
|
+
>
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Verify payment signature after Razorpay checkout success.
|
|
275
|
+
* Call with the payload from the Razorpay success handler (razorpay_payment_id, razorpay_subscription_id, razorpay_signature).
|
|
276
|
+
* Invalidates subscriptions list on success.
|
|
277
|
+
*/
|
|
278
|
+
export function useVerifyPayment(
|
|
279
|
+
client: RazorpayAuthClient | null | undefined,
|
|
280
|
+
options?: UseVerifyPaymentOptions
|
|
281
|
+
) {
|
|
282
|
+
const queryClient = useQueryClient()
|
|
283
|
+
return useMutation({
|
|
284
|
+
mutationFn: (input: VerifyPaymentInput) => verifyPayment(client!, input),
|
|
285
|
+
...options,
|
|
286
|
+
onSuccess: (data, variables, onMutateResult, context) => {
|
|
287
|
+
queryClient.invalidateQueries({ queryKey: razorpayQueryKeys.subscriptions() })
|
|
288
|
+
options?.onSuccess?.(data, variables, onMutateResult, context)
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
}
|
package/client/types.ts
CHANGED
|
@@ -106,3 +106,20 @@ export interface RestoreSubscriptionInput {
|
|
|
106
106
|
export interface ListSubscriptionsInput {
|
|
107
107
|
referenceId?: string
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
/** Input for verify-payment (Razorpay checkout success callback payload). */
|
|
111
|
+
export interface VerifyPaymentInput {
|
|
112
|
+
razorpay_payment_id: string
|
|
113
|
+
razorpay_subscription_id: string
|
|
114
|
+
razorpay_signature: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Response shape for verify-payment (success). */
|
|
118
|
+
export interface VerifyPaymentResponse {
|
|
119
|
+
success: true
|
|
120
|
+
data: {
|
|
121
|
+
message: string
|
|
122
|
+
payment_id: string
|
|
123
|
+
subscription_id: string
|
|
124
|
+
}
|
|
125
|
+
}
|
package/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getPlans,
|
|
6
6
|
listSubscriptions,
|
|
7
7
|
restoreSubscription,
|
|
8
|
+
verifyPayment,
|
|
8
9
|
webhook,
|
|
9
10
|
} from './api'
|
|
10
11
|
import type { RazorpayPluginOptions, RazorpayUserRecord } from './lib'
|
|
@@ -21,6 +22,7 @@ import type { RazorpayPluginOptions, RazorpayUserRecord } from './lib'
|
|
|
21
22
|
* @param options - Plugin configuration
|
|
22
23
|
* @param options.razorpayClient - Initialized Razorpay instance (key_id, key_secret)
|
|
23
24
|
* @param options.razorpayWebhookSecret - Webhook secret for signature verification
|
|
25
|
+
* @param options.razorpayKeySecret - API key secret for payment signature verification (optional; when set, enables POST /razorpay/verify-payment)
|
|
24
26
|
* @param options.createCustomerOnSignUp - Create Razorpay customer when user signs up (default: false)
|
|
25
27
|
* @param options.onCustomerCreate - Callback after customer is created
|
|
26
28
|
* @param options.getCustomerCreateParams - Custom params when creating customer
|
|
@@ -31,6 +33,7 @@ export const razorpayPlugin = (options: RazorpayPluginOptions) => {
|
|
|
31
33
|
const {
|
|
32
34
|
razorpayClient,
|
|
33
35
|
razorpayWebhookSecret,
|
|
36
|
+
razorpayKeySecret,
|
|
34
37
|
createCustomerOnSignUp = false,
|
|
35
38
|
onCustomerCreate,
|
|
36
39
|
getCustomerCreateParams,
|
|
@@ -83,6 +86,9 @@ export const razorpayPlugin = (options: RazorpayPluginOptions) => {
|
|
|
83
86
|
'subscription/restore': restoreSubscription(razorpay),
|
|
84
87
|
'subscription/list': listSubscriptions({ subscription: subOpts }),
|
|
85
88
|
'get-plans': getPlans({ subscription: subOpts }),
|
|
89
|
+
...(razorpayKeySecret
|
|
90
|
+
? { 'verify-payment': verifyPayment(razorpayKeySecret) }
|
|
91
|
+
: {}),
|
|
86
92
|
webhook: webhook(razorpayWebhookSecret, options.onWebhookEvent ?? undefined, {
|
|
87
93
|
subscription: subOpts,
|
|
88
94
|
onEvent,
|
package/lib/types.ts
CHANGED
|
@@ -169,6 +169,8 @@ export interface RazorpayPluginOptions {
|
|
|
169
169
|
razorpayClient: import('razorpay')
|
|
170
170
|
/** Webhook secret for signature verification. */
|
|
171
171
|
razorpayWebhookSecret?: string
|
|
172
|
+
/** API key secret for payment signature verification. When set, enables POST /razorpay/verify-payment (same secret as Razorpay client, not webhook secret). */
|
|
173
|
+
razorpayKeySecret?: string
|
|
172
174
|
/** Create Razorpay customer when user signs up. Default: false. */
|
|
173
175
|
createCustomerOnSignUp?: boolean
|
|
174
176
|
/** Called after a Razorpay customer is created. */
|