@paykit-sdk/paypal 1.0.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @paykit-sdk/paypal
2
+
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - added changeset
8
+ - Updated dependencies
9
+ - @paykit-sdk/core@1.1.93
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # @paykit-sdk/paypal
2
+
3
+ PayPal provider for PayKit
4
+
5
+ ## Quick Start
6
+
7
+ ```typescript
8
+ import { createEndpointHandlers, PayKit } from '@paykit-sdk/core';
9
+ import { paypal, createPayPal } from '@paykit-sdk/paypal';
10
+
11
+ // Method 1: Using environment variables
12
+ const provider = paypal(); // Ensure PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_SANDBOX are set
13
+
14
+ // Method 2: Direct configuration
15
+ const provider = createPayPal({
16
+ clientId: process.env.PAYPAL_CLIENT_ID,
17
+ clientSecret: process.env.PAYPAL_CLIENT_SECRET,
18
+ isSandbox: true,
19
+ });
20
+
21
+ export const paykit = new PayKit(provider);
22
+ export const endpoints = createEndpointHandlers(paykit);
23
+ ```
24
+
25
+ ### Next.js Catch All API Route (/api/paykit/[...endpoint]/route.ts)
26
+
27
+ ```typescript
28
+ import { endpoints } from '@/lib/paykit';
29
+ import { EndpointPath } from '@paykit-sdk/core';
30
+ import { NextRequest, NextResponse } from 'next/server';
31
+
32
+ export async function POST(
33
+ request: NextRequest,
34
+ { params }: { params: { endpoint: string[] } },
35
+ ) {
36
+ try {
37
+ // Construct the endpoint path with full type safety
38
+ const endpoint = ('/' + params.endpoint.join('/')) as EndpointPath;
39
+ const handler = endpoints[endpoint];
40
+
41
+ if (!handler) {
42
+ return NextResponse.json({ message: 'Endpoint not found' }, { status: 404 });
43
+ }
44
+
45
+ // Parse request body
46
+ const body = await request.json();
47
+ const { args } = body;
48
+
49
+ const result = await handler(...args);
50
+
51
+ return NextResponse.json({ result });
52
+ } catch (error) {
53
+ console.error('PayKit API Error:', error);
54
+ return NextResponse.json(
55
+ { message: error instanceof Error ? error.message : 'Internal server error' },
56
+ { status: 500 },
57
+ );
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### Next.js Webhooks (/api/paykit/webhooks/route.ts)
63
+
64
+ ```typescript
65
+ import { paykit } from '@/lib/paykit';
66
+ import { NextRequest, NextResponse } from 'next/server';
67
+
68
+ export async function POST(request: NextRequest) {
69
+ const webhookSecret = process.env.PAYPAL_WEBHOOK_ID;
70
+
71
+ if (!webhookSecret) {
72
+ return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 });
73
+ }
74
+
75
+ const webhook = paykit.webhooks
76
+ .setup({ webhookSecret })
77
+ .on('customer.created', async event => {
78
+ console.log('Customer created:', event.data);
79
+ })
80
+ .on('subscription.created', async event => {
81
+ console.log('Subscription created:', event.data);
82
+ })
83
+ .on('payment.created', async event => {
84
+ console.log('Payment created:', event.data);
85
+ })
86
+ .on('refund.created', async event => {
87
+ console.log('Refund created:', event.data);
88
+ });
89
+
90
+ const body = await request.text();
91
+ const headers = Object.fromEntries(request.headers.entries());
92
+ const url = request.url;
93
+ await webhook.handle({ body, headers, fullUrl: url });
94
+
95
+ // Return immediately, processing happens in background
96
+ return NextResponse.json({ success: true });
97
+ }
98
+ ```
99
+
100
+ ### Express.js with Webhook Handler
101
+
102
+ ```typescript
103
+ import { endpoints, paykit } from '@/lib/paykit';
104
+ import { EndpointPath } from '@paykit-sdk/core';
105
+ import express from 'express';
106
+
107
+ const app = express();
108
+
109
+ // IMPORTANT: Webhook route must come BEFORE express.json() middleware
110
+ // This ensures we get the raw body for signature verification
111
+ app.post(
112
+ '/api/webhooks/paypal',
113
+ express.raw({ type: 'application/json' }),
114
+ async (req, res) => {
115
+ try {
116
+ const webhookSecret = process.env.PAYPAL_WEBHOOK_ID;
117
+
118
+ if (!webhookSecret) {
119
+ return res.status(500).json({ error: 'Webhook secret not configured' });
120
+ }
121
+
122
+ const webhook = paykit.webhooks
123
+ .setup({ webhookSecret })
124
+ .on('customer.created', async event => {
125
+ console.log('Customer created:', event.data);
126
+ })
127
+ .on('subscription.created', async event => {
128
+ console.log('Subscription created:', event.data);
129
+ })
130
+ .on('payment.created', async event => {
131
+ console.log('Payment created:', event.data);
132
+ })
133
+ .on('refund.created', async event => {
134
+ console.log('Refund created:', event.data);
135
+ });
136
+
137
+ const body = req.body; // Raw buffer from express.raw()
138
+ const headers = req.headers;
139
+ const url = req.url;
140
+ await webhook.handle({ body, headers, fullUrl: url });
141
+
142
+ // Return immediately, processing happens in background
143
+ res.json({ success: true });
144
+ } catch (error) {
145
+ console.error('Webhook error:', error);
146
+ res.status(500).json({
147
+ message: error instanceof Error ? error.message : 'Webhook processing failed',
148
+ });
149
+ }
150
+ },
151
+ );
152
+
153
+ // Regular API routes use JSON middleware
154
+ app.use(express.json());
155
+
156
+ app.post('/api/paykit/*', async (req, res) => {
157
+ try {
158
+ const endpoint = req.path.replace('/api/paykit', '') as EndpointPath;
159
+ const handler = endpoints[endpoint];
160
+
161
+ if (!handler) {
162
+ return res.status(404).json({ message: 'Endpoint not found' });
163
+ }
164
+
165
+ const { args } = req.body;
166
+ const result = await handler(...args);
167
+
168
+ res.json({ result });
169
+ } catch (error) {
170
+ res.status(500).json({
171
+ message: error instanceof Error ? error.message : 'Internal server error',
172
+ });
173
+ }
174
+ });
175
+
176
+ app.listen(3000, () => {
177
+ console.log('Server running on port 3000');
178
+ });
179
+ ```
180
+
181
+ ## Subscription Management
182
+
183
+ ```typescript
184
+ // Update subscription
185
+ await paykit.subscriptions.update('sub_123', {
186
+ metadata: { plan: 'enterprise' },
187
+ });
188
+
189
+ // Cancel subscription
190
+ await paykit.subscriptions.cancel('sub_123');
191
+ ```
192
+
193
+ ## Environment Variables
194
+
195
+ ```bash
196
+ PAYPAL_CLIENT_ID=your_client_id
197
+ PAYPAL_CLIENT_SECRET=your_client_secret
198
+ PAYPAL_WEBHOOK_ID=your_webhook_id
199
+ ```
200
+
201
+ ## Support
202
+
203
+ - [PayPal Documentation](https://developer.paypal.com/docs/api/overview/)
204
+
205
+ ## License
206
+
207
+ ISC
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@paykit-sdk/paypal",
3
+ "version": "1.0.1",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "keywords": [],
7
+ "author": "Emmanuel Odii",
8
+ "license": "ISC",
9
+ "peerDependencies": {
10
+ "@paykit-sdk/core": ">=1.1.93"
11
+ },
12
+ "devDependencies": {
13
+ "@paykit-sdk/core": "1.1.93"
14
+ },
15
+ "dependencies": {
16
+ "@paypal/paypal-server-sdk": "^1.1.0",
17
+ "zod": "^3.24.2"
18
+ },
19
+ "scripts": {
20
+ "test": "echo \"Error: no test specified\" && exit 1"
21
+ }
22
+ }
@@ -0,0 +1,118 @@
1
+ import { CreateSubscriptionSchema } from '@paykit-sdk/core';
2
+ import { ApiResponse, CustomError } from '@paypal/paypal-server-sdk';
3
+ import { BaseController } from '@paypal/paypal-server-sdk/dist/types/controllers/baseController';
4
+ import {
5
+ createSubscriptionApticSchema,
6
+ resumeSubscriptionApticSchemaRequest,
7
+ resumeSubscriptionApticSchemaRequest as cancelSubscriptionApticSchemaRequest,
8
+ subscriptionApticSchema,
9
+ } from '../schema';
10
+
11
+ export class SubscriptionsController extends BaseController {
12
+ private catchAllErrors(req: ReturnType<BaseController['createRequest']>) {
13
+ req.throwOn(
14
+ 400,
15
+ CustomError,
16
+ 'Request is not well-formed, syntactically incorrect, or violates schema.',
17
+ );
18
+ req.throwOn(
19
+ 401,
20
+ CustomError,
21
+ 'Authentication failed due to missing authorization header, or invalid authentication credentials.',
22
+ );
23
+ req.throwOn(
24
+ 422,
25
+ CustomError,
26
+ 'The requested action could not be performed, semantically incorrect, or failed business validation.',
27
+ );
28
+ }
29
+
30
+ /**
31
+ * @return Response from the API call
32
+ */
33
+ async createSubscription({
34
+ body,
35
+ }: {
36
+ body: CreateSubscriptionSchema;
37
+ }): Promise<ApiResponse<any>> {
38
+ const req = this.createRequest('POST', '/v1/billing/subscriptions');
39
+ const mapped = req.prepareArgs({ body: [body, createSubscriptionApticSchema] });
40
+ req.header('Content-Type', 'application/json');
41
+ req.header('PayPal-Request-Id', Math.random().toString(36).substring(2, 15));
42
+
43
+ req.json(mapped.body);
44
+ this.catchAllErrors(req);
45
+ req.authenticate([{ oauth2: true }]);
46
+ return req.callAsJson(subscriptionApticSchema);
47
+ }
48
+
49
+ /**
50
+ * @returns Response from the API call
51
+ */
52
+ async resumeSubscription({
53
+ body,
54
+ subscriptionId,
55
+ }: {
56
+ body: { reason: string };
57
+ subscriptionId: string;
58
+ }) {
59
+ const req = this.createRequest(
60
+ 'POST',
61
+ `v1/billing/subscriptions/${subscriptionId}/activate `,
62
+ );
63
+ const mapped = req.prepareArgs({
64
+ body: [body, resumeSubscriptionApticSchemaRequest],
65
+ });
66
+ req.header('Content-Type', 'application/json');
67
+ req.header('PayPal-Request-Id', Math.random().toString(36).substring(2, 15));
68
+
69
+ req.json(mapped.body);
70
+ this.catchAllErrors(req);
71
+ req.authenticate([{ oauth2: true }]);
72
+ }
73
+
74
+ async retrieveSubscription({ subscriptionId }: { subscriptionId: string }) {
75
+ const req = this.createRequest('GET', `v1/billing/subscriptions/${subscriptionId}`);
76
+ req.header('PayPal-Request-Id', Math.random().toString(36).substring(2, 15));
77
+ this.catchAllErrors(req);
78
+ req.authenticate([{ oauth2: true }]);
79
+ return req.callAsJson(subscriptionApticSchema);
80
+ }
81
+
82
+ async cancelSubscription({
83
+ subscriptionId,
84
+ reason,
85
+ }: {
86
+ subscriptionId: string;
87
+ reason: string;
88
+ }) {
89
+ const req = this.createRequest(
90
+ 'POST',
91
+ `v1/billing/subscriptions/${subscriptionId}/cancel`,
92
+ );
93
+ const mapped = req.prepareArgs({
94
+ body: [reason, cancelSubscriptionApticSchemaRequest],
95
+ });
96
+ req.header('PayPal-Request-Id', Math.random().toString(36).substring(2, 15));
97
+ req.json(mapped.body);
98
+ this.catchAllErrors(req);
99
+ req.authenticate([{ oauth2: true }]);
100
+ return req.callAsJson(subscriptionApticSchema);
101
+ }
102
+
103
+ async updateSubscription({
104
+ subscriptionId,
105
+ metadata,
106
+ }: {
107
+ subscriptionId: string;
108
+ metadata: Record<string, unknown>;
109
+ }) {
110
+ const req = this.createRequest('PATCH', `v1/billing/subscriptions/${subscriptionId}`);
111
+ req.header('PayPal-Request-Id', Math.random().toString(36).substring(2, 15));
112
+
113
+ req.json({ op: 'replace', path: '/custom_id', value: JSON.stringify(metadata) });
114
+
115
+ this.catchAllErrors(req);
116
+ req.authenticate([{ oauth2: true }]);
117
+ }
118
+ }
@@ -0,0 +1,45 @@
1
+ import { ApiResponse, CustomError } from '@paypal/paypal-server-sdk';
2
+ import { BaseController } from '@paypal/paypal-server-sdk/dist/types/controllers/baseController';
3
+ import { VerifyWebhookSchema, verifyWebhookSchema } from '../schema';
4
+
5
+ export class WebhookController extends BaseController {
6
+ async verifyWebhook(body: {
7
+ authAlgo: string;
8
+ certUrl: string;
9
+ transmissionId: string;
10
+ transmissionSig: string;
11
+ transmissionTime: string;
12
+ webhookId: string;
13
+ webhookEvent: string;
14
+ }): Promise<ApiResponse<VerifyWebhookSchema>> {
15
+ const req = this.createRequest('POST', '/v1/notifications/verify-webhook-signature');
16
+ req.header('Content-Type', 'application/json');
17
+
18
+ req.throwOn(
19
+ 400,
20
+ CustomError,
21
+ 'Request is not well-formed, syntactically incorrect, or violates schema.',
22
+ );
23
+ req.throwOn(
24
+ 401,
25
+ CustomError,
26
+ 'Authentication failed due to missing authorization header, or invalid authentication credentials.',
27
+ );
28
+ req.throwOn(
29
+ 422,
30
+ CustomError,
31
+ 'The requested action could not be performed, semantically incorrect, or failed business validation.',
32
+ );
33
+ req.json(
34
+ Object.fromEntries(
35
+ Object.entries(body).map(([key, value]) => [
36
+ key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`),
37
+ value,
38
+ ]),
39
+ ),
40
+ );
41
+
42
+ req.authenticate([{ oauth2: true }]);
43
+ return req.callAsJson(verifyWebhookSchema);
44
+ }
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { validateRequiredKeys } from '@paykit-sdk/core';
2
+ import { PayPalOptions, PayPalProvider } from './paypal-provider';
3
+
4
+ export const paypal = () => {
5
+ const envVars = validateRequiredKeys(
6
+ ['PAYPAL_CLIENT_ID', 'PAYPAL_CLIENT_SECRET', 'PAYPAL_SANDBOX'],
7
+ process.env as Record<string, string>,
8
+ 'Missing required environment variables: {keys}',
9
+ );
10
+
11
+ return new PayPalProvider({
12
+ clientId: envVars.PAYPAL_CLIENT_ID,
13
+ clientSecret: envVars.PAYPAL_CLIENT_SECRET,
14
+ isSandbox: envVars.PAYPAL_SANDBOX === 'true',
15
+ });
16
+ };
17
+
18
+ export const createPayPal = (config: PayPalOptions) => {
19
+ return new PayPalProvider(config);
20
+ };
@@ -0,0 +1,504 @@
1
+ import {
2
+ PayKitProvider,
3
+ Checkout,
4
+ CreateCheckoutSchema,
5
+ CreateCustomerParams,
6
+ Customer,
7
+ UpdateCustomerParams,
8
+ Subscription,
9
+ UpdateSubscriptionSchema,
10
+ paykitEvent$InboundSchema,
11
+ WebhookEventPayload,
12
+ PaykitProviderOptions,
13
+ HandleWebhookParams,
14
+ UpdateCheckoutSchema,
15
+ CreateSubscriptionSchema,
16
+ CreatePaymentSchema,
17
+ Payment,
18
+ UpdatePaymentSchema,
19
+ CreateRefundSchema,
20
+ Refund,
21
+ validateRequiredKeys,
22
+ ProviderNotSupportedError,
23
+ ConstraintViolationError,
24
+ ResourceNotFoundError,
25
+ WebhookError,
26
+ NotImplementedError,
27
+ schema,
28
+ AbstractPayKitProvider,
29
+ } from '@paykit-sdk/core';
30
+ import {
31
+ CheckoutPaymentIntent,
32
+ Client,
33
+ Environment,
34
+ LogLevel,
35
+ Order,
36
+ OrdersController,
37
+ PaymentsController,
38
+ Refund as PayPalRefund,
39
+ OrderApplicationContextUserAction,
40
+ } from '@paypal/paypal-server-sdk';
41
+ import { z } from 'zod';
42
+ import { SubscriptionsController } from './controllers/subscription';
43
+ import { WebhookController } from './controllers/webhook';
44
+ import { VerifyWebhookStatus } from './schema';
45
+ import {
46
+ paykitCheckout$InboundSchema,
47
+ paykitPayment$InboundSchema,
48
+ paykitRefund$InboundSchema,
49
+ } from './utils/mapper';
50
+
51
+ const PAYPAL_METADATA_MAX_LENGTH = 127;
52
+
53
+ export interface PayPalOptions extends PaykitProviderOptions {
54
+ /**
55
+ * The client ID for the PayPal API
56
+ */
57
+ clientId: string;
58
+
59
+ /**
60
+ * The client secret for the PayPal API
61
+ */
62
+ clientSecret: string;
63
+
64
+ /**
65
+ * Whether to use the sandbox environment
66
+ */
67
+ isSandbox: boolean;
68
+ }
69
+
70
+ const paypalOptionsSchema = schema<PayPalOptions>()(
71
+ z.object({
72
+ clientId: z.string(),
73
+ clientSecret: z.string(),
74
+ isSandbox: z.boolean(),
75
+ debug: z.boolean().optional(),
76
+ }),
77
+ );
78
+
79
+ const providerName = 'paypal';
80
+ export class PayPalProvider extends AbstractPayKitProvider implements PayKitProvider {
81
+ readonly providerName = providerName;
82
+
83
+ private client: Client;
84
+ private ordersController: OrdersController;
85
+ private paymentsController: PaymentsController;
86
+ private subscriptionsController: SubscriptionsController;
87
+ private webhookController: WebhookController;
88
+
89
+ constructor(config: PayPalOptions) {
90
+ super(paypalOptionsSchema, config, providerName);
91
+
92
+ const { clientId, clientSecret, isSandbox = true, debug } = config;
93
+
94
+ const environment = isSandbox ? Environment.Sandbox : Environment.Production;
95
+
96
+ this.client = new Client({
97
+ clientCredentialsAuthCredentials: {
98
+ oAuthClientId: clientId,
99
+ oAuthClientSecret: clientSecret,
100
+ },
101
+ timeout: 0,
102
+ environment,
103
+ logging: {
104
+ logLevel: debug ? LogLevel.Info : LogLevel.Error,
105
+ logRequest: { logBody: debug },
106
+ logResponse: { logHeaders: debug },
107
+ },
108
+ });
109
+
110
+ this.ordersController = new OrdersController(this.client);
111
+ this.paymentsController = new PaymentsController(this.client);
112
+ this.subscriptionsController = new SubscriptionsController(this.client);
113
+ this.webhookController = new WebhookController(this.client);
114
+ }
115
+
116
+ /**
117
+ * Checkout management
118
+ * In PayPal, Order IS the checkout
119
+ */
120
+ createCheckout = async (params: CreateCheckoutSchema): Promise<Checkout> => {
121
+ const stringifiedMetadata = JSON.stringify(params.metadata);
122
+
123
+ if (stringifiedMetadata.length > PAYPAL_METADATA_MAX_LENGTH) {
124
+ throw new ConstraintViolationError('Metadata exceeds maximum length', {
125
+ value: stringifiedMetadata.length,
126
+ limit: PAYPAL_METADATA_MAX_LENGTH,
127
+ provider: this.providerName,
128
+ });
129
+ }
130
+ const {
131
+ currency = 'USD',
132
+ amount = '0',
133
+ itemName = 'Untitled Item',
134
+ } = validateRequiredKeys(
135
+ ['currency', 'amount', 'itemName'],
136
+ params.provider_metadata as Record<string, string>,
137
+ 'Missing required parameters: {keys}',
138
+ );
139
+
140
+ const orderOptionsBody: Parameters<OrdersController['createOrder']>[0]['body'] = {
141
+ intent: CheckoutPaymentIntent.Capture,
142
+ payer: {
143
+ ...(typeof params.customer === 'string' && { payerId: params.customer }),
144
+ ...(typeof params.customer === 'object' &&
145
+ 'email' in params.customer && { emailAddress: params.customer.email }),
146
+ },
147
+ purchaseUnits: [
148
+ {
149
+ amount: { currencyCode: currency, value: amount },
150
+ customId: stringifiedMetadata,
151
+ items: [
152
+ {
153
+ sku: params.item_id,
154
+ quantity: params.quantity.toString(),
155
+ name: itemName,
156
+ unitAmount: { currencyCode: currency, value: amount },
157
+ },
158
+ ],
159
+ },
160
+ ],
161
+ applicationContext: { userAction: OrderApplicationContextUserAction.PayNow },
162
+ ...(params.provider_metadata && { ...params.provider_metadata }),
163
+ };
164
+
165
+ if (params.billing) {
166
+ orderOptionsBody.purchaseUnits[0].shipping = {
167
+ name: { fullName: params.billing.address.name },
168
+ address: {
169
+ addressLine1: params.billing.address.line1,
170
+ addressLine2: params.billing.address.line2,
171
+ adminArea1: params.billing.address.city,
172
+ adminArea2: params.billing.address.state,
173
+ postalCode: params.billing.address.postal_code,
174
+ countryCode: params.billing.address.country,
175
+ },
176
+ ...(params.billing.address.phone && {
177
+ phoneNumber: {
178
+ nationalNumber: params.billing.address.phone,
179
+ countryCode: params.billing.address.country,
180
+ },
181
+ }),
182
+ };
183
+ }
184
+
185
+ const order = await this.ordersController.createOrder({ body: orderOptionsBody });
186
+
187
+ return paykitCheckout$InboundSchema(order.result);
188
+ };
189
+
190
+ retrieveCheckout = async (id: string): Promise<Checkout> => {
191
+ const order = await this.ordersController.getOrder({ id });
192
+
193
+ return paykitCheckout$InboundSchema(order.result);
194
+ };
195
+
196
+ updateCheckout = async (
197
+ id: string,
198
+ params: UpdateCheckoutSchema,
199
+ ): Promise<Checkout> => {
200
+ throw new ProviderNotSupportedError('updateCheckout', this.providerName, {
201
+ reason:
202
+ 'PayPal does not support updating orders. Cancel and create a new order instead.',
203
+ });
204
+ };
205
+
206
+ deleteCheckout = async (id: string): Promise<null> => {
207
+ throw new ProviderNotSupportedError('deleteCheckout', this.providerName, {
208
+ reason: 'PayPal orders cannot be deleted. They expire automatically.',
209
+ });
210
+ };
211
+
212
+ createCustomer = async (params: CreateCustomerParams): Promise<Customer> => {
213
+ throw new ProviderNotSupportedError('customer management', this.providerName, {
214
+ reason: 'PayPal does not have standalone customer entities.',
215
+ alternative: 'Use Payer information within orders or implement PayPal Vault API',
216
+ });
217
+ };
218
+
219
+ updateCustomer = async (
220
+ id: string,
221
+ params: UpdateCustomerParams,
222
+ ): Promise<Customer> => {
223
+ throw new ProviderNotSupportedError('updateCustomer', this.providerName, {
224
+ reason: 'PayPal does not support standalone customer management.',
225
+ });
226
+ };
227
+
228
+ deleteCustomer = async (id: string): Promise<null> => {
229
+ throw new ProviderNotSupportedError('deleteCustomer', this.providerName, {
230
+ reason: 'PayPal does not support standalone customer management.',
231
+ });
232
+ };
233
+
234
+ retrieveCustomer = async (id: string): Promise<Customer | null> => {
235
+ throw new ProviderNotSupportedError('retrieveCustomer', this.providerName, {
236
+ reason: 'PayPal does not support standalone customer management.',
237
+ });
238
+ };
239
+
240
+ /**
241
+ * Subscription management
242
+ * Would need PayPal Subscriptions API - different from Orders
243
+ */
244
+ createSubscription = async (
245
+ params: CreateSubscriptionSchema,
246
+ ): Promise<Subscription> => {
247
+ const subscription = await this.subscriptionsController.createSubscription({
248
+ body: params,
249
+ });
250
+ return subscription as unknown as Subscription;
251
+ };
252
+
253
+ cancelSubscription = async (id: string): Promise<Subscription> => {
254
+ const subscription = await this.subscriptionsController.cancelSubscription({
255
+ subscriptionId: id,
256
+ reason: 'Customer requested cancellation',
257
+ });
258
+
259
+ return subscription as unknown as Subscription;
260
+ };
261
+
262
+ updateSubscription = async (
263
+ id: string,
264
+ params: UpdateSubscriptionSchema,
265
+ ): Promise<Subscription> => {
266
+ const stringifiedMetadata = JSON.stringify(params.metadata);
267
+
268
+ if (stringifiedMetadata.length > PAYPAL_METADATA_MAX_LENGTH) {
269
+ throw new ConstraintViolationError('Metadata exceeds maximum length', {
270
+ value: stringifiedMetadata.length,
271
+ limit: PAYPAL_METADATA_MAX_LENGTH,
272
+ provider: this.providerName,
273
+ });
274
+ }
275
+ const subscription = await this.subscriptionsController.updateSubscription({
276
+ subscriptionId: id,
277
+ metadata: params.metadata ?? {},
278
+ });
279
+
280
+ return subscription as unknown as Subscription;
281
+ };
282
+
283
+ retrieveSubscription = async (id: string): Promise<Subscription> => {
284
+ const subscription = await this.subscriptionsController.retrieveSubscription({
285
+ subscriptionId: id,
286
+ });
287
+
288
+ return subscription as unknown as Subscription;
289
+ };
290
+
291
+ deleteSubscription = async (id: string): Promise<null> => {
292
+ throw new NotImplementedError('deleteSubscription', 'PayPal', {
293
+ futureSupport: false,
294
+ });
295
+ };
296
+
297
+ /**
298
+ * Payment management
299
+ * In PayPal, Order IS the payment
300
+ */
301
+ createPayment = async (params: CreatePaymentSchema): Promise<Payment> => {
302
+ const stringifiedMetadata = JSON.stringify(params.metadata);
303
+
304
+ if (stringifiedMetadata.length > PAYPAL_METADATA_MAX_LENGTH) {
305
+ throw new ConstraintViolationError('Metadata exceeds maximum length', {
306
+ value: stringifiedMetadata.length,
307
+ limit: PAYPAL_METADATA_MAX_LENGTH,
308
+ provider: this.providerName,
309
+ });
310
+ }
311
+
312
+ const orderOptionsBody: Parameters<OrdersController['createOrder']>[0]['body'] = {
313
+ intent: CheckoutPaymentIntent.Capture,
314
+ payer: {
315
+ ...(typeof params.customer === 'string' && { payerId: params.customer }),
316
+ ...(typeof params.customer === 'object' &&
317
+ 'email' in params.customer && { emailAddress: params.customer.email }),
318
+ },
319
+ purchaseUnits: [
320
+ {
321
+ amount: { currencyCode: params.currency, value: params.amount.toString() },
322
+ customId: stringifiedMetadata,
323
+ },
324
+ ],
325
+ };
326
+
327
+ if (params.billing) {
328
+ orderOptionsBody.purchaseUnits[0].shipping = {
329
+ name: { fullName: params.billing.address.name },
330
+ address: {
331
+ addressLine1: params.billing.address.line1,
332
+ addressLine2: params.billing.address.line2,
333
+ adminArea1: params.billing.address.city,
334
+ adminArea2: params.billing.address.state,
335
+ postalCode: params.billing.address.postal_code,
336
+ countryCode: params.billing.address.country,
337
+ },
338
+ ...(params.billing.address.phone && {
339
+ phoneNumber: {
340
+ nationalNumber: params.billing.address.phone,
341
+ countryCode: params.billing.address.country,
342
+ },
343
+ }),
344
+ };
345
+ }
346
+
347
+ const order = await this.ordersController.createOrder({ body: orderOptionsBody });
348
+
349
+ return paykitPayment$InboundSchema(order.result);
350
+ };
351
+
352
+ updatePayment = async (id: string, params: UpdatePaymentSchema): Promise<Payment> => {
353
+ throw new ProviderNotSupportedError('updatePayment', this.providerName, {
354
+ reason: 'PayPal does not support updating orders.',
355
+ });
356
+ };
357
+
358
+ retrievePayment = async (id: string): Promise<Payment | null> => {
359
+ const order = await this.ordersController.getOrder({ id });
360
+
361
+ return paykitPayment$InboundSchema(order.result);
362
+ };
363
+
364
+ deletePayment = async (id: string): Promise<null> => {
365
+ throw new ProviderNotSupportedError('deletePayment', this.providerName, {
366
+ reason: 'PayPal orders cannot be deleted. They expire automatically.',
367
+ });
368
+ };
369
+
370
+ capturePayment = async (id: string): Promise<Payment> => {
371
+ const captured = await this.ordersController.captureOrder({ id });
372
+ return paykitPayment$InboundSchema(captured.result);
373
+ };
374
+
375
+ cancelPayment = async (id: string): Promise<Payment> => {
376
+ // PayPal doesn't have explicit cancel, but you can void authorizations
377
+ throw new ProviderNotSupportedError('cancelPayment', this.providerName, {
378
+ reason:
379
+ 'PayPal order cancellation not directly supported. Orders expire automatically.',
380
+ });
381
+ };
382
+
383
+ /**
384
+ * Refund management
385
+ */
386
+ createRefund = async (params: CreateRefundSchema): Promise<Refund> => {
387
+ const order = await this.ordersController.getOrder({ id: params.payment_id });
388
+
389
+ const captureIds =
390
+ order.result.purchaseUnits?.[0]?.payments?.captures?.map(c => c.id!) || [];
391
+
392
+ if (captureIds.length === 0) {
393
+ throw new ResourceNotFoundError('Capture', params.payment_id, this.providerName);
394
+ }
395
+
396
+ const currencyCode = order.result.purchaseUnits?.[0]?.amount?.currencyCode || 'USD';
397
+ const amount = params.amount
398
+ ? params.amount.toString()
399
+ : order.result.purchaseUnits?.[0]?.amount?.value || '0';
400
+
401
+ const refund = await this.paymentsController.refundCapturedPayment({
402
+ captureId: captureIds[0],
403
+ body: { amount: { currencyCode, value: amount } },
404
+ });
405
+
406
+ return paykitRefund$InboundSchema(refund.result);
407
+ };
408
+
409
+ /**
410
+ * Webhook management
411
+ */
412
+ handleWebhook = async (
413
+ params: HandleWebhookParams,
414
+ ): Promise<Array<WebhookEventPayload>> => {
415
+ const { body, headers, webhookSecret: webhookId } = params;
416
+
417
+ const { result } = await this.webhookController.verifyWebhook({
418
+ authAlgo: headers.get('paypal-auth-algo') as string,
419
+ certUrl: headers.get('paypal-cert-url') as string,
420
+ transmissionId: headers.get('paypal-transmission-id') as string,
421
+ transmissionSig: headers.get('paypal-transmission-sig') as string,
422
+ transmissionTime: headers.get('paypal-transmission-time') as string,
423
+ webhookId,
424
+ webhookEvent: JSON.parse(body),
425
+ });
426
+
427
+ if (result.verification_status !== VerifyWebhookStatus.SUCCESS) {
428
+ throw new WebhookError('Webhook verification failed', {
429
+ provider: this.providerName,
430
+ });
431
+ }
432
+
433
+ const event = JSON.parse(body);
434
+ const eventType = event.event_type;
435
+
436
+ const webhookHandlers: Record<string, () => Promise<Array<WebhookEventPayload>>> = {
437
+ 'CHECKOUT.ORDER.APPROVED': async () => {
438
+ const orderData = event.resource as Order;
439
+ const payment = paykitPayment$InboundSchema(orderData);
440
+
441
+ return [
442
+ paykitEvent$InboundSchema<Payment>({
443
+ type: 'payment.created',
444
+ created: Date.now() / 1000,
445
+ id: event.id,
446
+ data: payment,
447
+ }),
448
+ ];
449
+ },
450
+
451
+ 'CHECKOUT.ORDER.COMPLETED': async () => {
452
+ const orderData = event.resource as Order;
453
+ const payment = paykitPayment$InboundSchema(orderData);
454
+
455
+ return [
456
+ paykitEvent$InboundSchema<Payment>({
457
+ type: 'payment.updated',
458
+ created: Date.now() / 1000,
459
+ id: event.id,
460
+ data: payment,
461
+ }),
462
+ ];
463
+ },
464
+
465
+ 'PAYMENT.CAPTURE.COMPLETED': async () => {
466
+ const orderData = event.resource as Order;
467
+ const payment = paykitPayment$InboundSchema(orderData);
468
+
469
+ return [
470
+ paykitEvent$InboundSchema<Payment>({
471
+ type: 'payment.updated',
472
+ created: Date.now() / 1000,
473
+ id: event.id,
474
+ data: payment,
475
+ }),
476
+ ];
477
+ },
478
+
479
+ 'PAYMENT.CAPTURE.REFUNDED': async () => {
480
+ const refundData = event.resource as PayPalRefund;
481
+ const refund = paykitRefund$InboundSchema(refundData);
482
+
483
+ return [
484
+ paykitEvent$InboundSchema<Refund>({
485
+ type: 'refund.created',
486
+ created: Date.now() / 1000,
487
+ id: event.id,
488
+ data: refund,
489
+ }),
490
+ ];
491
+ },
492
+ };
493
+
494
+ const handler = webhookHandlers[eventType];
495
+
496
+ if (!handler) {
497
+ throw new WebhookError(`Unhandled event type: ${eventType}`, {
498
+ provider: this.providerName,
499
+ });
500
+ }
501
+
502
+ return await handler();
503
+ };
504
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,230 @@
1
+ import {
2
+ lazy,
3
+ object,
4
+ Schema,
5
+ number,
6
+ string,
7
+ stringEnum,
8
+ boolean,
9
+ } from '@paypal/paypal-server-sdk/dist/types/schema';
10
+
11
+ export interface ShippingAmount {
12
+ /**
13
+ * The currency code.
14
+ */
15
+ currency_code: string;
16
+
17
+ /**
18
+ * The value which might be an integer for currencies like JPY that are not typically fractional.
19
+ */
20
+ value: string;
21
+ }
22
+
23
+ export const shippingAmountSchema: Schema<ShippingAmount> = object({
24
+ currency_code: ['currency_code', string()],
25
+ value: ['value', string()],
26
+ });
27
+
28
+ export interface CreateSubscriptionSchema {
29
+ /**
30
+ * The ID of the plan, maps to `item_id` in the Paykit SDK.
31
+ */
32
+ plan_id: string;
33
+
34
+ /**
35
+ * The quantity of the plan.
36
+ */
37
+ quantity: number;
38
+
39
+ /**
40
+ * The custom ID of the subscription.
41
+ */
42
+ custom_id: string;
43
+
44
+ /**
45
+ * The start time of the subscription.
46
+ */
47
+ start_time: string;
48
+
49
+ /**
50
+ * The subscriber of the subscription.
51
+ */
52
+ subscriber: {
53
+ email_address: string;
54
+ name: { given_name: string; surname: string };
55
+ phone: { phone_number: string };
56
+ };
57
+ }
58
+
59
+ export const createSubscriptionApticSchema: Schema<CreateSubscriptionSchema> = object({
60
+ plan_id: ['plan_id', string()],
61
+ quantity: ['quantity', number()],
62
+ custom_id: ['custom_id', string()],
63
+ start_time: ['start_time', string()],
64
+ subscriber: [
65
+ 'subscriber',
66
+ lazy(() =>
67
+ object({
68
+ email_address: ['email_address', string()],
69
+ name: [
70
+ 'name',
71
+ lazy(() =>
72
+ object({
73
+ given_name: ['given_name', string()],
74
+ surname: ['surname', string()],
75
+ }),
76
+ ),
77
+ ],
78
+ phone: [
79
+ 'phone',
80
+ lazy(() => object({ phone_number: ['phone_number', string()] })),
81
+ ],
82
+ }),
83
+ ),
84
+ ],
85
+ });
86
+
87
+ enum SubscriptionStatus {
88
+ APPROVAL_PENDING = 'APPROVAL_PENDING',
89
+ APPROVED = 'APPROVED',
90
+ ACTIVE = 'ACTIVE',
91
+ SUSPENDED = 'SUSPENDED',
92
+ CANCELLED = 'CANCELLED',
93
+ EXPIRED = 'EXPIRED',
94
+ }
95
+
96
+ export interface Subscription {
97
+ /**
98
+ * The ID of the subscription.
99
+ */
100
+ id: string;
101
+
102
+ /**
103
+ * The ID of the plan.
104
+ */
105
+ plan_id: string;
106
+
107
+ /**
108
+ * The quantity of the subscription.
109
+ */
110
+ quantity: number;
111
+
112
+ /**
113
+ * The status of the subscription.
114
+ */
115
+ status: SubscriptionStatus;
116
+
117
+ /**
118
+ * The time the status was updated.
119
+ */
120
+ status_update_time: string;
121
+
122
+ /**
123
+ * The note for the status change.
124
+ */
125
+ status_change_note: string;
126
+
127
+ /**
128
+ * The time the subscription started.
129
+ */
130
+ start_time: string;
131
+
132
+ /**
133
+ * The subscriber of the subscription.
134
+ */
135
+ subscriber: {
136
+ /**
137
+ * The email address of the subscriber.
138
+ */
139
+ email_address: string;
140
+
141
+ /**
142
+ * The name of the subscriber.
143
+ */
144
+ name: {
145
+ /**
146
+ * The given name of the subscriber.
147
+ */
148
+ given_name: string;
149
+ /**
150
+ * The surname of the subscriber.
151
+ */
152
+ surname: string;
153
+ };
154
+
155
+ /**
156
+ * The ID of the payer.
157
+ */
158
+ payer_id: string;
159
+ };
160
+
161
+ /**
162
+ * Whether the plan was overridden.
163
+ */
164
+ plan_overridden: boolean;
165
+
166
+ /**
167
+ * The custom ID of the subscription.
168
+ */
169
+ custom_id: string;
170
+ }
171
+
172
+ export const subscriptionApticSchema: Schema<Subscription> = object({
173
+ id: ['id', string()],
174
+ plan_id: ['plan_id', string()],
175
+ quantity: ['quantity', number()],
176
+ custom_id: ['custom_id', string()],
177
+ plan_overridden: ['plan_overridden', boolean()],
178
+ start_time: ['start_time', string()],
179
+ subscriber: [
180
+ 'subscriber',
181
+ lazy(() =>
182
+ object({
183
+ email_address: ['email_address', string()],
184
+ name: [
185
+ 'name',
186
+ lazy(() =>
187
+ object({
188
+ given_name: ['given_name', string()],
189
+ surname: ['surname', string()],
190
+ }),
191
+ ),
192
+ ],
193
+ payer_id: ['payer_id', string()],
194
+ }),
195
+ ),
196
+ ],
197
+ status: ['status', stringEnum(SubscriptionStatus)],
198
+ status_change_note: ['status_change_note', string()],
199
+ status_update_time: ['status_update_time', string()],
200
+ });
201
+
202
+ interface ResumeSubscriptionSchema {
203
+ /**
204
+ * The reason for resuming the subscription.
205
+ */
206
+ reason: string;
207
+ }
208
+
209
+ export const resumeSubscriptionApticSchemaRequest: Schema<ResumeSubscriptionSchema> =
210
+ object({
211
+ reason: ['reason', string()],
212
+ });
213
+
214
+ // WEBHOOK
215
+
216
+ export enum VerifyWebhookStatus {
217
+ SUCCESS = 'SUCCESS',
218
+ FAILURE = 'FAILURE',
219
+ }
220
+
221
+ export interface VerifyWebhookSchema {
222
+ /**
223
+ * The status of the verification.
224
+ */
225
+ verification_status: VerifyWebhookStatus;
226
+ }
227
+
228
+ export const verifyWebhookSchema: Schema<VerifyWebhookSchema> = object({
229
+ verification_status: ['verification_status', stringEnum(VerifyWebhookStatus)],
230
+ });
package/src/types.ts ADDED
@@ -0,0 +1,126 @@
1
+ export type SubscriptionStatus =
2
+ | 'APPROVAL_PENDING'
3
+ | 'APPROVED'
4
+ | 'ACTIVE'
5
+ | 'SUSPENDED'
6
+ | 'CANCELLED'
7
+ | 'EXPIRED';
8
+
9
+ export interface PayPalSubscription {
10
+ /**
11
+ * The ID of the subscription.
12
+ */
13
+ id: string;
14
+
15
+ /**
16
+ * The ID of the plan.
17
+ */
18
+ plan_id: string;
19
+
20
+ /**
21
+ * The status of the subscription.
22
+ */
23
+ status: SubscriptionStatus;
24
+
25
+ /**
26
+ * The time the status was updated.
27
+ */
28
+ status_update_time?: string;
29
+
30
+ /**
31
+ * The time the subscription started.
32
+ */
33
+ start_time?: string;
34
+
35
+ /**
36
+ * The time the subscription was created.
37
+ */
38
+ create_time?: string;
39
+
40
+ /**
41
+ * The subscriber of the subscription.
42
+ */
43
+ subscriber?: {
44
+ /**
45
+ * The email of the subscriber.
46
+ */
47
+ email_address?: string;
48
+
49
+ /**
50
+ * The name of the subscriber.
51
+ */
52
+ name?: {
53
+ /**
54
+ * The given name of the subscriber.
55
+ */
56
+ given_name?: string;
57
+ /**
58
+ * The surname of the subscriber.
59
+ */
60
+ surname?: string;
61
+ };
62
+ };
63
+
64
+ /**
65
+ * The billing info of the subscription.
66
+ */
67
+ billing_info?: {
68
+ /**
69
+ * The outstanding balance of the subscription.
70
+ */
71
+ outstanding_balance?: {
72
+ /**
73
+ * The currency code of the outstanding balance.
74
+ */
75
+ currency_code?: string;
76
+ /**
77
+ * The value of the outstanding balance.
78
+ */
79
+ value?: string;
80
+ };
81
+ cycle_executions?: Array<{
82
+ /**
83
+ * The tenure type of the cycle execution.
84
+ */
85
+ tenure_type?: string;
86
+
87
+ /**
88
+ * The sequence of the cycle execution.
89
+ */
90
+ sequence?: number;
91
+
92
+ /**
93
+ * The number of cycles completed.
94
+ */
95
+ cycles_completed?: number;
96
+
97
+ /**
98
+ * The total number of cycles.
99
+ */
100
+ total_cycles?: number;
101
+ }>;
102
+ };
103
+
104
+ /**
105
+ * The links of the subscription.
106
+ */
107
+ links?: Array<{
108
+ /**
109
+ * The href of the link.
110
+ */
111
+ href?: string;
112
+ /**
113
+ * The rel of the link.
114
+ */
115
+ rel?: string;
116
+ /**
117
+ * The method of the link.
118
+ */
119
+ method?: string;
120
+ }>;
121
+
122
+ /**
123
+ * The custom ID of the subscription, i.e metadata
124
+ */
125
+ customId?: string;
126
+ }
@@ -0,0 +1,111 @@
1
+ import {
2
+ Checkout,
3
+ omitInternalMetadata,
4
+ Payment,
5
+ Refund,
6
+ Subscription,
7
+ } from '@paykit-sdk/core';
8
+ import { Order, Refund as PayPalRefund } from '@paypal/paypal-server-sdk';
9
+ import { PayPalSubscription } from '../types';
10
+
11
+ /**
12
+ * Refund
13
+ */
14
+ export const paykitRefund$InboundSchema = (refund: PayPalRefund): Refund => {
15
+ return {
16
+ id: refund.id!,
17
+ amount: refund.amount?.value ? parseFloat(refund.amount.value) : 0,
18
+ currency: refund.amount?.currencyCode || 'USD',
19
+ metadata: refund.customId ? omitInternalMetadata(JSON.parse(refund.customId)) : {},
20
+ reason: refund.noteToPayer ?? null,
21
+ };
22
+ };
23
+
24
+ /**
25
+ * Checkout
26
+ */
27
+ export const paykitCheckout$InboundSchema = (order: Order): Checkout => {
28
+ return {
29
+ id: order.id!,
30
+ payment_url: order.links?.find(l => l.rel === 'approve')?.href || '',
31
+ amount: parseFloat(order.purchaseUnits?.[0]?.amount?.value || '0'),
32
+ currency: order.purchaseUnits?.[0]?.amount?.currencyCode || 'USD',
33
+ customer: order.payer?.payerId
34
+ ? order.payer?.payerId
35
+ : { email: order.payer?.emailAddress ?? '' },
36
+ session_type: 'one_time',
37
+ products: [{ id: order.purchaseUnits?.[0]?.items?.[0]?.sku || '', quantity: 1 }],
38
+ metadata: order.purchaseUnits?.[0]?.customId
39
+ ? omitInternalMetadata(JSON.parse(order.purchaseUnits?.[0]?.customId))
40
+ : {},
41
+ subscription: null,
42
+ };
43
+ };
44
+
45
+ /**
46
+ * Payment
47
+ */
48
+ export const paykitPayment$InboundSchema = (order: Order): Payment => {
49
+ const statusMap: Record<string, Payment['status']> = {
50
+ CREATED: 'pending',
51
+ SAVED: 'pending',
52
+ APPROVED: 'requires_capture',
53
+ VOIDED: 'canceled',
54
+ COMPLETED: 'succeeded',
55
+ PAYER_ACTION_REQUIRED: 'requires_action',
56
+ };
57
+
58
+ const status = statusMap[order.status ?? statusMap.CREATED];
59
+
60
+ return {
61
+ id: order.id!,
62
+ status,
63
+ amount: parseFloat(order.purchaseUnits?.[0]?.amount?.value || '0'),
64
+ currency: order.purchaseUnits?.[0]?.amount?.currencyCode || 'USD',
65
+ metadata: order.purchaseUnits?.[0]?.customId
66
+ ? omitInternalMetadata(JSON.parse(order.purchaseUnits?.[0]?.customId))
67
+ : {},
68
+ customer: order.payer?.payerId
69
+ ? order.payer?.payerId
70
+ : { email: order.payer?.emailAddress ?? '' },
71
+ item_id: order.purchaseUnits?.[0]?.items?.[0]?.sku || '',
72
+ };
73
+ };
74
+
75
+ /**
76
+ * Subscription
77
+ */
78
+ export const paykitSubscription$InboundSchema = (
79
+ subscription: PayPalSubscription,
80
+ ): Subscription => {
81
+ const statusMap: Record<string, Subscription['status']> = {
82
+ APPROVAL_PENDING: 'pending',
83
+ APPROVED: 'active',
84
+ ACTIVE: 'active',
85
+ SUSPENDED: 'past_due',
86
+ CANCELLED: 'canceled',
87
+ EXPIRED: 'canceled',
88
+ };
89
+
90
+ const status = statusMap[subscription.status ?? statusMap.APPROVAL_PENDING];
91
+
92
+ return {
93
+ id: subscription.id,
94
+ customer: { email: subscription.subscriber?.email_address ?? '' },
95
+ status,
96
+ item_id: subscription.plan_id,
97
+ current_period_start: subscription.start_time
98
+ ? new Date(subscription.start_time)
99
+ : new Date(),
100
+ current_period_end: subscription.status_update_time
101
+ ? new Date(subscription.status_update_time)
102
+ : new Date(), // todo: Would need to calculate based on billing cycle
103
+ metadata: subscription.customId
104
+ ? omitInternalMetadata(JSON.parse(subscription.customId))
105
+ : {},
106
+ billing_interval: 'month',
107
+ amount: 0,
108
+ currency: 'USD',
109
+ custom_fields: null,
110
+ };
111
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }