@deiondz/better-auth-razorpay 1.0.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/index.ts ADDED
@@ -0,0 +1,123 @@
1
+ import type { BetterAuthPlugin } from 'better-auth'
2
+ import Razorpay from 'razorpay'
3
+ import {
4
+ cancelSubscription,
5
+ getPlans,
6
+ getSubscription,
7
+ pauseSubscription,
8
+ resumeSubscription,
9
+ subscribe,
10
+ verifyPayment,
11
+ webhook,
12
+ } from './api'
13
+ import type { RazorpayPluginOptions } from './lib'
14
+
15
+ /**
16
+ * Razorpay plugin for Better Auth.
17
+ *
18
+ * Provides subscription management functionality including:
19
+ * - Creating subscriptions
20
+ * - Managing subscription lifecycle (pause, resume, cancel)
21
+ * - Payment verification
22
+ * - Webhook handling for subscription events
23
+ * - Plan retrieval
24
+ *
25
+ * @param options - Plugin configuration options
26
+ * @param options.keyId - Razorpay key ID (required)
27
+ * @param options.keySecret - Razorpay key secret (required)
28
+ * @param options.webhookSecret - Webhook secret for signature verification (optional)
29
+ * @param options.plans - Array of plan IDs from Razorpay dashboard (required)
30
+ * @param options.onWebhookEvent - Optional callback for custom webhook event handling
31
+ * @returns Better Auth plugin configuration
32
+ *
33
+ * @throws {Error} If keyId or keySecret are not provided
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * import { razorpayPlugin } from '@better-auth/razorpay'
38
+ *
39
+ * const auth = betterAuth({
40
+ * plugins: [
41
+ * razorpayPlugin({
42
+ * keyId: process.env.RAZORPAY_KEY_ID!,
43
+ * keySecret: process.env.RAZORPAY_KEY_SECRET!,
44
+ * webhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
45
+ * plans: ['plan_1234567890', 'plan_0987654321'],
46
+ * }),
47
+ * ],
48
+ * })
49
+ * ```
50
+ */
51
+ export const razorpayPlugin = (options: RazorpayPluginOptions) => {
52
+ const { keyId, keySecret, webhookSecret, plans, onWebhookEvent } = options
53
+
54
+ if (!keyId || !keySecret) {
55
+ throw new Error('Razorpay keyId and keySecret are required')
56
+ }
57
+
58
+ // Initialize Razorpay instance once in plugin closure
59
+ // This instance is accessible to all endpoints via closure scope
60
+ const razorpay = new Razorpay({
61
+ key_id: keyId,
62
+ key_secret: keySecret,
63
+ })
64
+
65
+ return {
66
+ id: 'razorpay-plugin',
67
+
68
+ schema: {
69
+ user: {
70
+ fields: {
71
+ subscriptionId: { type: 'string', required: false },
72
+ subscriptionPlanId: { type: 'string', required: false },
73
+ subscriptionStatus: { type: 'string', required: false },
74
+ subscriptionCurrentPeriodEnd: { type: 'date', required: false },
75
+ cancelAtPeriodEnd: { type: 'boolean', required: false },
76
+ lastPaymentDate: { type: 'date', required: false },
77
+ nextBillingDate: { type: 'date', required: false },
78
+ },
79
+ },
80
+ razorpayCustomer: {
81
+ fields: {
82
+ userId: { type: 'string', unique: true },
83
+ razorpayCustomerId: { type: 'string', unique: true },
84
+ },
85
+ },
86
+ razorpaySubscription: {
87
+ fields: {
88
+ userId: { type: 'string' },
89
+ subscriptionId: { type: 'string' },
90
+ planId: { type: 'string' },
91
+ status: { type: 'string' },
92
+ },
93
+ },
94
+ },
95
+
96
+ endpoints: {
97
+ subscribe: subscribe(razorpay, plans),
98
+ 'get-plans': getPlans(razorpay, plans),
99
+ 'verify-payment': verifyPayment(keySecret),
100
+ webhook: webhook(webhookSecret, onWebhookEvent),
101
+ 'get-subscription': getSubscription(razorpay),
102
+ 'pause-subscription': pauseSubscription(razorpay),
103
+ 'cancel-subscription': cancelSubscription(razorpay),
104
+ 'resume-subscription': resumeSubscription(razorpay),
105
+ },
106
+ } satisfies BetterAuthPlugin
107
+ }
108
+
109
+ // Re-export types for external usage
110
+ export type {
111
+ OnWebhookEventCallback,
112
+ RazorpayApiResponse,
113
+ RazorpayErrorResponse,
114
+ RazorpayPluginOptions,
115
+ RazorpaySubscription,
116
+ RazorpaySubscriptionRecord,
117
+ RazorpaySuccessResponse,
118
+ RazorpayUserRecord,
119
+ RazorpayWebhookContext,
120
+ RazorpayWebhookEvent,
121
+ RazorpayWebhookPayload,
122
+ } from './lib'
123
+ export type { WebhookResult } from './api/webhook'
@@ -0,0 +1,99 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * Helper function to handle Razorpay-related errors and reduce complexity.
5
+ * Handles various error types including validation, network, timeout, and Razorpay API errors.
6
+ */
7
+ function handleRazorpayError(error: unknown): {
8
+ success: false
9
+ error: { code: string; description: string }
10
+ } {
11
+ // Handle Zod validation errors
12
+ if (error instanceof z.ZodError) {
13
+ return {
14
+ success: false,
15
+ error: {
16
+ code: 'INVALID_REQUEST',
17
+ description: error.issues[0]?.message || 'Validation failed',
18
+ },
19
+ }
20
+ }
21
+
22
+ // Handle network errors (fetch/axios network failures)
23
+ if (error && typeof error === 'object') {
24
+ // Check for network error indicators
25
+ const errorObj = error as Record<string, unknown>
26
+ if (
27
+ 'code' in errorObj &&
28
+ (errorObj.code === 'ECONNREFUSED' ||
29
+ errorObj.code === 'ENOTFOUND' ||
30
+ errorObj.code === 'ETIMEDOUT' ||
31
+ errorObj.code === 'ECONNRESET')
32
+ ) {
33
+ return {
34
+ success: false,
35
+ error: {
36
+ code: 'NETWORK_ERROR',
37
+ description: 'Network connection failed. Please check your internet connection and try again.',
38
+ },
39
+ }
40
+ }
41
+
42
+ // Handle timeout errors
43
+ if (
44
+ 'name' in errorObj &&
45
+ (errorObj.name === 'TimeoutError' ||
46
+ errorObj.name === 'AbortError' ||
47
+ (typeof errorObj.message === 'string' &&
48
+ errorObj.message.toLowerCase().includes('timeout')))
49
+ ) {
50
+ return {
51
+ success: false,
52
+ error: {
53
+ code: 'TIMEOUT_ERROR',
54
+ description: 'Request timed out. Please try again.',
55
+ },
56
+ }
57
+ }
58
+
59
+ // Handle Razorpay error format: { error: { code: string, description: string } }
60
+ if ('error' in errorObj) {
61
+ const razorpayError = errorObj as {
62
+ error?: { code?: string; description?: string; field?: string }
63
+ }
64
+ return {
65
+ success: false,
66
+ error: {
67
+ code: razorpayError.error?.code || 'RAZORPAY_ERROR',
68
+ description:
69
+ razorpayError.error?.description ||
70
+ razorpayError.error?.field
71
+ ? `Razorpay error: ${razorpayError.error.field}`
72
+ : 'Razorpay request failed',
73
+ },
74
+ }
75
+ }
76
+
77
+ // Handle standard error objects with message
78
+ if ('message' in errorObj && typeof errorObj.message === 'string') {
79
+ return {
80
+ success: false,
81
+ error: {
82
+ code: 'UNKNOWN_ERROR',
83
+ description: errorObj.message,
84
+ },
85
+ }
86
+ }
87
+ }
88
+
89
+ // Fallback for unknown error types
90
+ return {
91
+ success: false,
92
+ error: {
93
+ code: 'UNKNOWN_ERROR',
94
+ description: 'An unexpected error occurred. Please try again or contact support.',
95
+ },
96
+ }
97
+ }
98
+
99
+ export { handleRazorpayError }
package/lib/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ export { handleRazorpayError } from './error-handler'
2
+ export {
3
+ cancelSubscriptionSchema,
4
+ getPlansSchema,
5
+ pauseSubscriptionSchema,
6
+ resumeSubscriptionSchema,
7
+ subscribeSchema,
8
+ verifyPaymentSchema,
9
+ } from './schemas'
10
+ export type {
11
+ OnWebhookEventCallback,
12
+ RazorpayApiResponse,
13
+ RazorpayErrorResponse,
14
+ RazorpayPluginOptions,
15
+ RazorpaySubscription,
16
+ RazorpaySubscriptionRecord,
17
+ RazorpaySuccessResponse,
18
+ RazorpayUserRecord,
19
+ RazorpayWebhookContext,
20
+ RazorpayWebhookEvent,
21
+ RazorpayWebhookPayload,
22
+ } from './types'
package/lib/schemas.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod'
2
+
3
+ // Schema for getPlans validation (GET requests don't have a body, so optional)
4
+ const getPlansSchema = z.object({}).strict().optional()
5
+
6
+ // Schema for subscribe validation
7
+ const subscribeSchema = z.object({
8
+ plan_id: z.string().min(1, 'Plan ID is required'),
9
+ total_count: z.number().int().min(1, 'Total count must be at least 1').optional().default(12),
10
+ quantity: z.number().int().min(1, 'Quantity must be at least 1').optional().default(1),
11
+ start_at: z.number().int().optional(),
12
+ expire_by: z.number().int().optional(),
13
+ customer_notify: z.boolean().optional().default(true),
14
+ addons: z
15
+ .array(
16
+ z.object({
17
+ item: z.object({
18
+ name: z.string().min(1, 'Addon name is required'),
19
+ amount: z.number().int().positive('Addon amount must be positive'),
20
+ currency: z.string().min(1, 'Addon currency is required'),
21
+ }),
22
+ })
23
+ )
24
+ .optional(),
25
+ offer_id: z.string().optional(),
26
+ notes: z.record(z.string(), z.string()).optional(),
27
+ })
28
+
29
+ // Schema for verify payment validation
30
+ const verifyPaymentSchema = z.object({
31
+ razorpay_payment_id: z.string().min(1, 'Payment ID is required'),
32
+ razorpay_subscription_id: z.string().min(1, 'Subscription ID is required'),
33
+ razorpay_signature: z.string().min(1, 'Signature is required'),
34
+ })
35
+
36
+ // Schema for pause subscription validation
37
+ const pauseSubscriptionSchema = z.object({
38
+ subscription_id: z.string().min(1, 'Subscription ID is required'),
39
+ })
40
+
41
+ // Schema for cancel subscription validation
42
+ const cancelSubscriptionSchema = z.object({
43
+ subscription_id: z.string().min(1, 'Subscription ID is required'),
44
+ })
45
+
46
+ // Schema for resume subscription validation
47
+ const resumeSubscriptionSchema = z.object({
48
+ subscription_id: z.string().min(1, 'Subscription ID is required'),
49
+ })
50
+
51
+ export {
52
+ getPlansSchema,
53
+ subscribeSchema,
54
+ verifyPaymentSchema,
55
+ pauseSubscriptionSchema,
56
+ cancelSubscriptionSchema,
57
+ resumeSubscriptionSchema,
58
+ }
package/lib/types.ts ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Razorpay subscription response interface from the Razorpay API.
3
+ * This represents the full subscription object returned by Razorpay API calls.
4
+ */
5
+ export interface RazorpaySubscription {
6
+ id: string
7
+ entity: string
8
+ plan_id: string
9
+ status: string
10
+ current_start: number
11
+ current_end: number
12
+ ended_at: number | null
13
+ quantity: number
14
+ notes: Record<string, string> | null
15
+ charge_at: number
16
+ start_at: number
17
+ end_at: number
18
+ auth_attempts: number
19
+ total_count: number
20
+ paid_count: number
21
+ customer_notify: boolean
22
+ created_at: number
23
+ expire_by: number | null
24
+ short_url: string
25
+ has_scheduled_changes: boolean
26
+ change_scheduled_at: number | null
27
+ source: string
28
+ offer_id: string | null
29
+ remaining_count: string
30
+ }
31
+
32
+ /**
33
+ * Subscription record stored in the auth adapter.
34
+ */
35
+ export interface RazorpaySubscriptionRecord {
36
+ userId: string
37
+ subscriptionId: string
38
+ planId: string
39
+ status: string
40
+ }
41
+
42
+ /**
43
+ * User record shape used by the Razorpay plugin.
44
+ */
45
+ export interface RazorpayUserRecord {
46
+ id: string
47
+ email?: string
48
+ name?: string
49
+ subscriptionId?: string
50
+ subscriptionPlanId?: string
51
+ subscriptionStatus?: string
52
+ subscriptionCurrentPeriodEnd?: Date | null
53
+ cancelAtPeriodEnd?: boolean
54
+ lastPaymentDate?: Date | null
55
+ nextBillingDate?: Date | null
56
+ }
57
+
58
+ type RazorpayWebhookEvent =
59
+ | 'subscription.authenticated'
60
+ | 'subscription.activated'
61
+ | 'subscription.charged'
62
+ | 'subscription.cancelled'
63
+ | 'subscription.paused'
64
+ | 'subscription.resumed'
65
+ | 'subscription.pending'
66
+ | 'subscription.halted'
67
+
68
+ interface RazorpayWebhookPayload {
69
+ event: RazorpayWebhookEvent
70
+ subscription: {
71
+ id: string
72
+ plan_id: string
73
+ status: string
74
+ current_start?: number
75
+ current_end?: number
76
+ [key: string]: unknown // Allow other Razorpay subscription fields
77
+ }
78
+ payment?: {
79
+ id: string
80
+ amount: number
81
+ currency: string
82
+ [key: string]: unknown // Allow other Razorpay payment fields
83
+ }
84
+ }
85
+
86
+ interface RazorpayWebhookContext {
87
+ userId: string
88
+ user: {
89
+ id: string
90
+ email: string
91
+ name: string
92
+ [key: string]: unknown // Allow other user fields
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Callback function invoked after webhook events are processed.
98
+ * Can be used for any custom logic: emails, notifications, analytics, integrations, etc.
99
+ */
100
+ type OnWebhookEventCallback = (
101
+ payload: RazorpayWebhookPayload,
102
+ context: RazorpayWebhookContext
103
+ ) => Promise<void>
104
+
105
+ interface RazorpayPluginOptions {
106
+ keyId: string
107
+ keySecret: string
108
+ webhookSecret?: string
109
+ plans: string[] // Array of plan IDs from Razorpay dashboard
110
+ /**
111
+ * Optional callback function invoked after webhook events are processed.
112
+ * Use this for any custom logic: sending emails, updating external systems,
113
+ * analytics tracking, integrations, or any other business logic.
114
+ */
115
+ onWebhookEvent?: OnWebhookEventCallback
116
+ }
117
+
118
+ /**
119
+ * Standard success response structure for Razorpay API endpoints.
120
+ */
121
+ export interface RazorpaySuccessResponse<T = unknown> {
122
+ success: true
123
+ data: T
124
+ }
125
+
126
+ /**
127
+ * Standard error response structure for Razorpay API endpoints.
128
+ */
129
+ export interface RazorpayErrorResponse {
130
+ success: false
131
+ error: {
132
+ code: string
133
+ description: string
134
+ [key: string]: unknown // Allow additional error metadata
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Union type for all Razorpay API responses.
140
+ */
141
+ export type RazorpayApiResponse<T = unknown> =
142
+ | RazorpaySuccessResponse<T>
143
+ | RazorpayErrorResponse
144
+
145
+ export type {
146
+ RazorpayPluginOptions,
147
+ RazorpayWebhookEvent,
148
+ RazorpayWebhookPayload,
149
+ RazorpayWebhookContext,
150
+ OnWebhookEventCallback,
151
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@deiondz/better-auth-razorpay",
3
+ "version": "1.0.0",
4
+ "description": "Better Auth plugin for Razorpay subscriptions and payments",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "types": "./index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.ts",
11
+ "import": "./index.ts",
12
+ "default": "./index.ts"
13
+ },
14
+ "./client": {
15
+ "types": "./client.ts",
16
+ "import": "./client.ts",
17
+ "default": "./client.ts"
18
+ }
19
+ },
20
+ "files": [
21
+ "index.ts",
22
+ "client.ts",
23
+ "api",
24
+ "lib",
25
+ "README.md",
26
+ "RAZORPAY_PLUGIN.md"
27
+ ],
28
+ "scripts": {
29
+ "typecheck": "tsc --noEmit",
30
+ "prepublishOnly": "npm run typecheck"
31
+ },
32
+ "keywords": [
33
+ "better-auth",
34
+ "razorpay",
35
+ "plugin",
36
+ "subscriptions",
37
+ "payments",
38
+ "authentication"
39
+ ],
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/deiondz/better-auth-razorpay.git"
44
+ },
45
+ "peerDependencies": {
46
+ "better-auth": "^1.0.0"
47
+ },
48
+ "dependencies": {
49
+ "razorpay": "^2.9.2",
50
+ "zod": "^3.23.0"
51
+ },
52
+ "devDependencies": {
53
+ "typescript": "^5.0.0"
54
+ },
55
+ "engines": {
56
+ "node": ">=18"
57
+ }
58
+ }