@xenterprises/fastify-xplaid 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/src/xPlaid.js ADDED
@@ -0,0 +1,764 @@
1
+ /**
2
+ * xPlaid - Plaid Financial Data Integration
3
+ *
4
+ * A Fastify plugin for integrating with Plaid's banking API.
5
+ * Provides Link token management, account access, transactions, balances, and identity verification.
6
+ *
7
+ * @see https://plaid.com/docs/
8
+ */
9
+
10
+ import fp from "fastify-plugin";
11
+ import { Configuration, PlaidApi, PlaidEnvironments, Products, CountryCode } from "plaid";
12
+
13
+ /**
14
+ * Plaid environments
15
+ */
16
+ const ENVIRONMENTS = {
17
+ SANDBOX: "sandbox",
18
+ DEVELOPMENT: "development",
19
+ PRODUCTION: "production",
20
+ };
21
+
22
+ /**
23
+ * Plaid products
24
+ */
25
+ const PRODUCTS = {
26
+ AUTH: "auth",
27
+ TRANSACTIONS: "transactions",
28
+ IDENTITY: "identity",
29
+ ASSETS: "assets",
30
+ INVESTMENTS: "investments",
31
+ LIABILITIES: "liabilities",
32
+ BALANCE: "balance",
33
+ INCOME: "income",
34
+ TRANSFER: "transfer",
35
+ SIGNAL: "signal",
36
+ STATEMENTS: "statements",
37
+ };
38
+
39
+ /**
40
+ * Country codes
41
+ */
42
+ const COUNTRY_CODES = {
43
+ US: "US",
44
+ CA: "CA",
45
+ GB: "GB",
46
+ ES: "ES",
47
+ FR: "FR",
48
+ IE: "IE",
49
+ NL: "NL",
50
+ DE: "DE",
51
+ };
52
+
53
+ /**
54
+ * xPlaid Plugin
55
+ *
56
+ * @param {import('fastify').FastifyInstance} fastify - Fastify instance
57
+ * @param {Object} options - Plugin options
58
+ * @param {string} options.clientId - Plaid client ID (required)
59
+ * @param {string} options.secret - Plaid secret (required)
60
+ * @param {string} [options.environment] - Plaid environment (sandbox, development, production)
61
+ * @param {string} [options.version] - Plaid API version (default: 2020-09-14)
62
+ * @param {boolean} [options.active] - Enable/disable the plugin (default: true)
63
+ */
64
+ async function xPlaid(fastify, options) {
65
+ // Check if plugin is disabled
66
+ if (options.active === false) {
67
+ fastify.log.info("# ⏸️ xPlaid Disabled");
68
+ return;
69
+ }
70
+
71
+ // Validate required configuration
72
+ if (!options.clientId) {
73
+ throw new Error("xPlaid: clientId is required");
74
+ }
75
+ if (!options.secret) {
76
+ throw new Error("xPlaid: secret is required");
77
+ }
78
+
79
+ const version = options.version || "2020-09-14";
80
+
81
+ // Determine Plaid environment and base path
82
+ let environment;
83
+ let basePath;
84
+ switch (options.environment) {
85
+ case ENVIRONMENTS.PRODUCTION:
86
+ environment = ENVIRONMENTS.PRODUCTION;
87
+ basePath = PlaidEnvironments.production;
88
+ break;
89
+ case ENVIRONMENTS.DEVELOPMENT:
90
+ environment = ENVIRONMENTS.DEVELOPMENT;
91
+ basePath = PlaidEnvironments.development;
92
+ break;
93
+ default:
94
+ environment = ENVIRONMENTS.SANDBOX;
95
+ basePath = PlaidEnvironments.sandbox;
96
+ }
97
+
98
+ // Configure Plaid client
99
+ const configuration = new Configuration({
100
+ basePath,
101
+ baseOptions: {
102
+ headers: {
103
+ "PLAID-CLIENT-ID": options.clientId,
104
+ "PLAID-SECRET": options.secret,
105
+ "Plaid-Version": version,
106
+ },
107
+ },
108
+ });
109
+
110
+ const plaidClient = new PlaidApi(configuration);
111
+
112
+ // ============================================================================
113
+ // Link Token Service
114
+ // ============================================================================
115
+
116
+ /**
117
+ * Create a Link token for initializing Plaid Link
118
+ * @param {Object} params - Link token parameters
119
+ * @param {string} params.userId - Unique client user ID
120
+ * @param {string} [params.clientName] - Application name (max 30 chars)
121
+ * @param {string[]} [params.products] - Products to enable
122
+ * @param {string[]} [params.countryCodes] - Country codes
123
+ * @param {string} [params.language] - Display language
124
+ * @param {string} [params.webhook] - Webhook URL
125
+ * @param {string} [params.redirectUri] - OAuth redirect URI
126
+ * @param {string} [params.accessToken] - For update mode
127
+ * @returns {Promise<Object>} Link token response
128
+ */
129
+ async function createLinkToken(params) {
130
+ const {
131
+ userId,
132
+ clientName = "App",
133
+ products = [Products.Transactions],
134
+ countryCodes = [CountryCode.Us],
135
+ language = "en",
136
+ webhook,
137
+ redirectUri,
138
+ accessToken,
139
+ } = params;
140
+
141
+ const request = {
142
+ user: {
143
+ client_user_id: userId,
144
+ },
145
+ client_name: clientName.slice(0, 30),
146
+ products: accessToken ? undefined : products,
147
+ country_codes: countryCodes,
148
+ language,
149
+ };
150
+
151
+ if (webhook) request.webhook = webhook;
152
+ if (redirectUri) request.redirect_uri = redirectUri;
153
+ if (accessToken) request.access_token = accessToken;
154
+
155
+ try {
156
+ const response = await plaidClient.linkTokenCreate(request);
157
+ return {
158
+ linkToken: response.data.link_token,
159
+ expiration: response.data.expiration,
160
+ requestId: response.data.request_id,
161
+ };
162
+ } catch (error) {
163
+ fastify.log.error({ error: error.response?.data }, "Plaid linkTokenCreate failed");
164
+ throw new Error(error.response?.data?.error_message || "Failed to create link token");
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Get information about a Link token
170
+ * @param {string} linkToken - Link token
171
+ * @returns {Promise<Object>} Link token info
172
+ */
173
+ async function getLinkToken(linkToken) {
174
+ try {
175
+ const response = await plaidClient.linkTokenGet({ link_token: linkToken });
176
+ return response.data;
177
+ } catch (error) {
178
+ fastify.log.error({ error: error.response?.data }, "Plaid linkTokenGet failed");
179
+ throw new Error(error.response?.data?.error_message || "Failed to get link token");
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Exchange a public token for an access token
185
+ * @param {string} publicToken - Public token from Link
186
+ * @returns {Promise<Object>} Access token and item ID
187
+ */
188
+ async function exchangePublicToken(publicToken) {
189
+ try {
190
+ const response = await plaidClient.itemPublicTokenExchange({
191
+ public_token: publicToken,
192
+ });
193
+ return {
194
+ accessToken: response.data.access_token,
195
+ itemId: response.data.item_id,
196
+ requestId: response.data.request_id,
197
+ };
198
+ } catch (error) {
199
+ fastify.log.error({ error: error.response?.data }, "Plaid exchangePublicToken failed");
200
+ throw new Error(error.response?.data?.error_message || "Failed to exchange public token");
201
+ }
202
+ }
203
+
204
+ // ============================================================================
205
+ // Accounts Service
206
+ // ============================================================================
207
+
208
+ /**
209
+ * Get accounts for an Item
210
+ * @param {string} accessToken - Access token
211
+ * @param {Object} [options] - Options
212
+ * @param {string[]} [options.accountIds] - Filter by account IDs
213
+ * @returns {Promise<Object>} Accounts response
214
+ */
215
+ async function getAccounts(accessToken, options = {}) {
216
+ const request = { access_token: accessToken };
217
+
218
+ if (options.accountIds) {
219
+ request.options = { account_ids: options.accountIds };
220
+ }
221
+
222
+ try {
223
+ const response = await plaidClient.accountsGet(request);
224
+ return {
225
+ accounts: response.data.accounts,
226
+ item: response.data.item,
227
+ requestId: response.data.request_id,
228
+ };
229
+ } catch (error) {
230
+ fastify.log.error({ error: error.response?.data }, "Plaid accountsGet failed");
231
+ throw new Error(error.response?.data?.error_message || "Failed to get accounts");
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Get real-time balance for accounts
237
+ * @param {string} accessToken - Access token
238
+ * @param {Object} [options] - Options
239
+ * @param {string[]} [options.accountIds] - Filter by account IDs
240
+ * @returns {Promise<Object>} Balance response
241
+ */
242
+ async function getBalance(accessToken, options = {}) {
243
+ const request = { access_token: accessToken };
244
+
245
+ if (options.accountIds) {
246
+ request.options = { account_ids: options.accountIds };
247
+ }
248
+
249
+ try {
250
+ const response = await plaidClient.accountsBalanceGet(request);
251
+ return {
252
+ accounts: response.data.accounts,
253
+ item: response.data.item,
254
+ requestId: response.data.request_id,
255
+ };
256
+ } catch (error) {
257
+ fastify.log.error({ error: error.response?.data }, "Plaid accountsBalanceGet failed");
258
+ throw new Error(error.response?.data?.error_message || "Failed to get balance");
259
+ }
260
+ }
261
+
262
+ // ============================================================================
263
+ // Transactions Service
264
+ // ============================================================================
265
+
266
+ /**
267
+ * Sync transactions (recommended approach)
268
+ * @param {string} accessToken - Access token
269
+ * @param {Object} [options] - Options
270
+ * @param {string} [options.cursor] - Cursor for pagination
271
+ * @param {number} [options.count] - Number of transactions (max 500)
272
+ * @param {string[]} [options.accountIds] - Filter by account IDs
273
+ * @returns {Promise<Object>} Transaction sync response
274
+ */
275
+ async function syncTransactions(accessToken, options = {}) {
276
+ const request = { access_token: accessToken };
277
+
278
+ if (options.cursor) request.cursor = options.cursor;
279
+ if (options.count) request.count = Math.min(options.count, 500);
280
+ if (options.accountIds) {
281
+ request.options = { account_ids: options.accountIds };
282
+ }
283
+
284
+ try {
285
+ const response = await plaidClient.transactionsSync(request);
286
+ return {
287
+ added: response.data.added,
288
+ modified: response.data.modified,
289
+ removed: response.data.removed,
290
+ nextCursor: response.data.next_cursor,
291
+ hasMore: response.data.has_more,
292
+ requestId: response.data.request_id,
293
+ };
294
+ } catch (error) {
295
+ fastify.log.error({ error: error.response?.data }, "Plaid transactionsSync failed");
296
+ throw new Error(error.response?.data?.error_message || "Failed to sync transactions");
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Get transactions within a date range (legacy approach)
302
+ * @param {string} accessToken - Access token
303
+ * @param {string} startDate - Start date (YYYY-MM-DD)
304
+ * @param {string} endDate - End date (YYYY-MM-DD)
305
+ * @param {Object} [options] - Options
306
+ * @param {number} [options.count] - Number of transactions (max 500)
307
+ * @param {number} [options.offset] - Pagination offset
308
+ * @param {string[]} [options.accountIds] - Filter by account IDs
309
+ * @returns {Promise<Object>} Transactions response
310
+ */
311
+ async function getTransactions(accessToken, startDate, endDate, options = {}) {
312
+ const request = {
313
+ access_token: accessToken,
314
+ start_date: startDate,
315
+ end_date: endDate,
316
+ };
317
+
318
+ const requestOptions = {};
319
+ if (options.count) requestOptions.count = Math.min(options.count, 500);
320
+ if (options.offset) requestOptions.offset = options.offset;
321
+ if (options.accountIds) requestOptions.account_ids = options.accountIds;
322
+
323
+ if (Object.keys(requestOptions).length > 0) {
324
+ request.options = requestOptions;
325
+ }
326
+
327
+ try {
328
+ const response = await plaidClient.transactionsGet(request);
329
+ return {
330
+ accounts: response.data.accounts,
331
+ transactions: response.data.transactions,
332
+ totalTransactions: response.data.total_transactions,
333
+ item: response.data.item,
334
+ requestId: response.data.request_id,
335
+ };
336
+ } catch (error) {
337
+ fastify.log.error({ error: error.response?.data }, "Plaid transactionsGet failed");
338
+ throw new Error(error.response?.data?.error_message || "Failed to get transactions");
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Refresh transactions
344
+ * @param {string} accessToken - Access token
345
+ * @returns {Promise<Object>} Refresh response
346
+ */
347
+ async function refreshTransactions(accessToken) {
348
+ try {
349
+ const response = await plaidClient.transactionsRefresh({
350
+ access_token: accessToken,
351
+ });
352
+ return {
353
+ requestId: response.data.request_id,
354
+ };
355
+ } catch (error) {
356
+ fastify.log.error({ error: error.response?.data }, "Plaid transactionsRefresh failed");
357
+ throw new Error(error.response?.data?.error_message || "Failed to refresh transactions");
358
+ }
359
+ }
360
+
361
+ // ============================================================================
362
+ // Identity Service
363
+ // ============================================================================
364
+
365
+ /**
366
+ * Get identity information for accounts
367
+ * @param {string} accessToken - Access token
368
+ * @param {Object} [options] - Options
369
+ * @param {string[]} [options.accountIds] - Filter by account IDs
370
+ * @returns {Promise<Object>} Identity response
371
+ */
372
+ async function getIdentity(accessToken, options = {}) {
373
+ const request = { access_token: accessToken };
374
+
375
+ if (options.accountIds) {
376
+ request.options = { account_ids: options.accountIds };
377
+ }
378
+
379
+ try {
380
+ const response = await plaidClient.identityGet(request);
381
+ return {
382
+ accounts: response.data.accounts,
383
+ item: response.data.item,
384
+ requestId: response.data.request_id,
385
+ };
386
+ } catch (error) {
387
+ fastify.log.error({ error: error.response?.data }, "Plaid identityGet failed");
388
+ throw new Error(error.response?.data?.error_message || "Failed to get identity");
389
+ }
390
+ }
391
+
392
+ // ============================================================================
393
+ // Auth Service
394
+ // ============================================================================
395
+
396
+ /**
397
+ * Get account and routing numbers for ACH
398
+ * @param {string} accessToken - Access token
399
+ * @param {Object} [options] - Options
400
+ * @param {string[]} [options.accountIds] - Filter by account IDs
401
+ * @returns {Promise<Object>} Auth response
402
+ */
403
+ async function getAuth(accessToken, options = {}) {
404
+ const request = { access_token: accessToken };
405
+
406
+ if (options.accountIds) {
407
+ request.options = { account_ids: options.accountIds };
408
+ }
409
+
410
+ try {
411
+ const response = await plaidClient.authGet(request);
412
+ return {
413
+ accounts: response.data.accounts,
414
+ numbers: response.data.numbers,
415
+ item: response.data.item,
416
+ requestId: response.data.request_id,
417
+ };
418
+ } catch (error) {
419
+ fastify.log.error({ error: error.response?.data }, "Plaid authGet failed");
420
+ throw new Error(error.response?.data?.error_message || "Failed to get auth");
421
+ }
422
+ }
423
+
424
+ // ============================================================================
425
+ // Investments Service
426
+ // ============================================================================
427
+
428
+ /**
429
+ * Get investment holdings
430
+ * @param {string} accessToken - Access token
431
+ * @param {Object} [options] - Options
432
+ * @param {string[]} [options.accountIds] - Filter by account IDs
433
+ * @returns {Promise<Object>} Holdings response
434
+ */
435
+ async function getHoldings(accessToken, options = {}) {
436
+ const request = { access_token: accessToken };
437
+
438
+ if (options.accountIds) {
439
+ request.options = { account_ids: options.accountIds };
440
+ }
441
+
442
+ try {
443
+ const response = await plaidClient.investmentsHoldingsGet(request);
444
+ return {
445
+ accounts: response.data.accounts,
446
+ holdings: response.data.holdings,
447
+ securities: response.data.securities,
448
+ item: response.data.item,
449
+ requestId: response.data.request_id,
450
+ };
451
+ } catch (error) {
452
+ fastify.log.error({ error: error.response?.data }, "Plaid investmentsHoldingsGet failed");
453
+ throw new Error(error.response?.data?.error_message || "Failed to get holdings");
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Get investment transactions
459
+ * @param {string} accessToken - Access token
460
+ * @param {string} startDate - Start date (YYYY-MM-DD)
461
+ * @param {string} endDate - End date (YYYY-MM-DD)
462
+ * @param {Object} [options] - Options
463
+ * @param {number} [options.count] - Number of transactions (max 500)
464
+ * @param {number} [options.offset] - Pagination offset
465
+ * @param {string[]} [options.accountIds] - Filter by account IDs
466
+ * @returns {Promise<Object>} Investment transactions response
467
+ */
468
+ async function getInvestmentTransactions(accessToken, startDate, endDate, options = {}) {
469
+ const request = {
470
+ access_token: accessToken,
471
+ start_date: startDate,
472
+ end_date: endDate,
473
+ };
474
+
475
+ const requestOptions = {};
476
+ if (options.count) requestOptions.count = Math.min(options.count, 500);
477
+ if (options.offset) requestOptions.offset = options.offset;
478
+ if (options.accountIds) requestOptions.account_ids = options.accountIds;
479
+
480
+ if (Object.keys(requestOptions).length > 0) {
481
+ request.options = requestOptions;
482
+ }
483
+
484
+ try {
485
+ const response = await plaidClient.investmentsTransactionsGet(request);
486
+ return {
487
+ accounts: response.data.accounts,
488
+ investmentTransactions: response.data.investment_transactions,
489
+ securities: response.data.securities,
490
+ totalInvestmentTransactions: response.data.total_investment_transactions,
491
+ item: response.data.item,
492
+ requestId: response.data.request_id,
493
+ };
494
+ } catch (error) {
495
+ fastify.log.error({ error: error.response?.data }, "Plaid investmentsTransactionsGet failed");
496
+ throw new Error(
497
+ error.response?.data?.error_message || "Failed to get investment transactions"
498
+ );
499
+ }
500
+ }
501
+
502
+ // ============================================================================
503
+ // Liabilities Service
504
+ // ============================================================================
505
+
506
+ /**
507
+ * Get liabilities information
508
+ * @param {string} accessToken - Access token
509
+ * @param {Object} [options] - Options
510
+ * @param {string[]} [options.accountIds] - Filter by account IDs
511
+ * @returns {Promise<Object>} Liabilities response
512
+ */
513
+ async function getLiabilities(accessToken, options = {}) {
514
+ const request = { access_token: accessToken };
515
+
516
+ if (options.accountIds) {
517
+ request.options = { account_ids: options.accountIds };
518
+ }
519
+
520
+ try {
521
+ const response = await plaidClient.liabilitiesGet(request);
522
+ return {
523
+ accounts: response.data.accounts,
524
+ liabilities: response.data.liabilities,
525
+ item: response.data.item,
526
+ requestId: response.data.request_id,
527
+ };
528
+ } catch (error) {
529
+ fastify.log.error({ error: error.response?.data }, "Plaid liabilitiesGet failed");
530
+ throw new Error(error.response?.data?.error_message || "Failed to get liabilities");
531
+ }
532
+ }
533
+
534
+ // ============================================================================
535
+ // Item Management
536
+ // ============================================================================
537
+
538
+ /**
539
+ * Get Item information
540
+ * @param {string} accessToken - Access token
541
+ * @returns {Promise<Object>} Item response
542
+ */
543
+ async function getItem(accessToken) {
544
+ try {
545
+ const response = await plaidClient.itemGet({
546
+ access_token: accessToken,
547
+ });
548
+ return {
549
+ item: response.data.item,
550
+ status: response.data.status,
551
+ requestId: response.data.request_id,
552
+ };
553
+ } catch (error) {
554
+ fastify.log.error({ error: error.response?.data }, "Plaid itemGet failed");
555
+ throw new Error(error.response?.data?.error_message || "Failed to get item");
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Remove an Item
561
+ * @param {string} accessToken - Access token
562
+ * @returns {Promise<Object>} Remove response
563
+ */
564
+ async function removeItem(accessToken) {
565
+ try {
566
+ const response = await plaidClient.itemRemove({
567
+ access_token: accessToken,
568
+ });
569
+ return {
570
+ requestId: response.data.request_id,
571
+ };
572
+ } catch (error) {
573
+ fastify.log.error({ error: error.response?.data }, "Plaid itemRemove failed");
574
+ throw new Error(error.response?.data?.error_message || "Failed to remove item");
575
+ }
576
+ }
577
+
578
+ /**
579
+ * Get institution by ID
580
+ * @param {string} institutionId - Institution ID
581
+ * @param {string[]} [countryCodes] - Country codes
582
+ * @returns {Promise<Object>} Institution response
583
+ */
584
+ async function getInstitution(institutionId, countryCodes = [CountryCode.Us]) {
585
+ try {
586
+ const response = await plaidClient.institutionsGetById({
587
+ institution_id: institutionId,
588
+ country_codes: countryCodes,
589
+ });
590
+ return {
591
+ institution: response.data.institution,
592
+ requestId: response.data.request_id,
593
+ };
594
+ } catch (error) {
595
+ fastify.log.error({ error: error.response?.data }, "Plaid institutionsGetById failed");
596
+ throw new Error(error.response?.data?.error_message || "Failed to get institution");
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Search institutions
602
+ * @param {string} query - Search query
603
+ * @param {Object} [options] - Options
604
+ * @param {string[]} [options.countryCodes] - Country codes
605
+ * @param {string[]} [options.products] - Filter by products
606
+ * @param {number} [options.count] - Number of results
607
+ * @param {number} [options.offset] - Pagination offset
608
+ * @returns {Promise<Object>} Institutions search response
609
+ */
610
+ async function searchInstitutions(query, options = {}) {
611
+ const request = {
612
+ query,
613
+ country_codes: options.countryCodes || [CountryCode.Us],
614
+ };
615
+
616
+ if (options.products) request.products = options.products;
617
+ if (options.count) request.options = { ...request.options, count: options.count };
618
+ if (options.offset) request.options = { ...request.options, offset: options.offset };
619
+
620
+ try {
621
+ const response = await plaidClient.institutionsSearch(request);
622
+ return {
623
+ institutions: response.data.institutions,
624
+ requestId: response.data.request_id,
625
+ };
626
+ } catch (error) {
627
+ fastify.log.error({ error: error.response?.data }, "Plaid institutionsSearch failed");
628
+ throw new Error(error.response?.data?.error_message || "Failed to search institutions");
629
+ }
630
+ }
631
+
632
+ // ============================================================================
633
+ // Webhooks
634
+ // ============================================================================
635
+
636
+ /**
637
+ * Update webhook URL for an Item
638
+ * @param {string} accessToken - Access token
639
+ * @param {string} webhook - New webhook URL
640
+ * @returns {Promise<Object>} Update response
641
+ */
642
+ async function updateWebhook(accessToken, webhook) {
643
+ try {
644
+ const response = await plaidClient.itemWebhookUpdate({
645
+ access_token: accessToken,
646
+ webhook,
647
+ });
648
+ return {
649
+ item: response.data.item,
650
+ requestId: response.data.request_id,
651
+ };
652
+ } catch (error) {
653
+ fastify.log.error({ error: error.response?.data }, "Plaid itemWebhookUpdate failed");
654
+ throw new Error(error.response?.data?.error_message || "Failed to update webhook");
655
+ }
656
+ }
657
+
658
+ /**
659
+ * Get webhook verification key
660
+ * @param {string} keyId - Key ID from webhook header
661
+ * @returns {Promise<Object>} Key response
662
+ */
663
+ async function getWebhookVerificationKey(keyId) {
664
+ try {
665
+ const response = await plaidClient.webhookVerificationKeyGet({
666
+ key_id: keyId,
667
+ });
668
+ return {
669
+ key: response.data.key,
670
+ requestId: response.data.request_id,
671
+ };
672
+ } catch (error) {
673
+ fastify.log.error({ error: error.response?.data }, "Plaid webhookVerificationKeyGet failed");
674
+ throw new Error(error.response?.data?.error_message || "Failed to get verification key");
675
+ }
676
+ }
677
+
678
+ // Store configuration on fastify instance
679
+ fastify.decorate("xplaid", {
680
+ // Configuration
681
+ config: {
682
+ environment,
683
+ clientId: `***${options.clientId.slice(-4)}`,
684
+ },
685
+
686
+ // Constants
687
+ constants: {
688
+ ENVIRONMENTS,
689
+ PRODUCTS,
690
+ COUNTRY_CODES,
691
+ },
692
+
693
+ // Link token service
694
+ link: {
695
+ createToken: createLinkToken,
696
+ getToken: getLinkToken,
697
+ exchangePublicToken,
698
+ },
699
+
700
+ // Accounts service
701
+ accounts: {
702
+ get: getAccounts,
703
+ getBalance,
704
+ },
705
+
706
+ // Transactions service
707
+ transactions: {
708
+ sync: syncTransactions,
709
+ get: getTransactions,
710
+ refresh: refreshTransactions,
711
+ },
712
+
713
+ // Identity service
714
+ identity: {
715
+ get: getIdentity,
716
+ },
717
+
718
+ // Auth service (ACH)
719
+ auth: {
720
+ get: getAuth,
721
+ },
722
+
723
+ // Investments service
724
+ investments: {
725
+ getHoldings,
726
+ getTransactions: getInvestmentTransactions,
727
+ },
728
+
729
+ // Liabilities service
730
+ liabilities: {
731
+ get: getLiabilities,
732
+ },
733
+
734
+ // Item management service
735
+ items: {
736
+ get: getItem,
737
+ remove: removeItem,
738
+ getInstitution,
739
+ searchInstitutions,
740
+ },
741
+
742
+ // Webhooks service
743
+ webhooks: {
744
+ update: updateWebhook,
745
+ getVerificationKey: getWebhookVerificationKey,
746
+ },
747
+
748
+ // Raw client access
749
+ raw: plaidClient,
750
+ });
751
+
752
+ fastify.log.info(`# ✅ xPlaid Enabled (${environment})`);
753
+ fastify.log.info("# • Link Token Management");
754
+ fastify.log.info("# • Accounts & Balance");
755
+ fastify.log.info("# • Transactions");
756
+ fastify.log.info("# • Identity & Auth");
757
+ fastify.log.info("# • Investments & Liabilities");
758
+ }
759
+
760
+ export default fp(xPlaid, {
761
+ name: "xPlaid",
762
+ });
763
+
764
+ export { ENVIRONMENTS, PRODUCTS, COUNTRY_CODES };