@striderlabs/mcp-chase 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/browser.ts ADDED
@@ -0,0 +1,530 @@
1
+ /**
2
+ * Chase Bank Browser Automation
3
+ *
4
+ * Patchright-based stealth automation for Chase banking operations.
5
+ */
6
+
7
+ import { chromium, Browser, BrowserContext, Page } from "patchright";
8
+ import { saveCookies, loadCookies, AuthState } from "./auth.js";
9
+
10
+ const CHASE_BASE_URL = "https://www.chase.com";
11
+ const DEFAULT_TIMEOUT = 60000;
12
+
13
+ // Singleton browser instance
14
+ let browser: Browser | null = null;
15
+ let context: BrowserContext | null = null;
16
+ let page: Page | null = null;
17
+
18
+ export interface Account {
19
+ id: string;
20
+ name: string;
21
+ type: "checking" | "savings" | "credit" | "investment" | "loan";
22
+ balance: number;
23
+ availableBalance?: number;
24
+ accountNumber?: string;
25
+ lastFour?: string;
26
+ }
27
+
28
+ export interface Transaction {
29
+ id: string;
30
+ date: string;
31
+ description: string;
32
+ amount: number;
33
+ type: "credit" | "debit";
34
+ category?: string;
35
+ pending: boolean;
36
+ merchant?: string;
37
+ }
38
+
39
+ export interface Bill {
40
+ id: string;
41
+ payee: string;
42
+ amount: number;
43
+ dueDate: string;
44
+ status: "scheduled" | "paid" | "overdue";
45
+ autopay: boolean;
46
+ }
47
+
48
+ export interface Transfer {
49
+ id: string;
50
+ fromAccount: string;
51
+ toAccount: string;
52
+ amount: number;
53
+ date: string;
54
+ status: "pending" | "completed" | "failed";
55
+ }
56
+
57
+ export interface Statement {
58
+ id: string;
59
+ accountId: string;
60
+ period: string;
61
+ date: string;
62
+ downloadUrl?: string;
63
+ }
64
+
65
+ export interface RewardsInfo {
66
+ pointsBalance: number;
67
+ cashBackBalance: number;
68
+ pendingRewards: number;
69
+ tier?: string;
70
+ }
71
+
72
+ /**
73
+ * Initialize browser with stealth settings
74
+ */
75
+ async function initBrowser(): Promise<void> {
76
+ if (browser) return;
77
+
78
+ browser = await chromium.launch({
79
+ headless: true,
80
+ args: [
81
+ "--disable-blink-features=AutomationControlled",
82
+ "--no-sandbox",
83
+ "--disable-setuid-sandbox",
84
+ "--disable-web-security",
85
+ "--disable-features=IsolateOrigins,site-per-process",
86
+ ],
87
+ });
88
+
89
+ context = await browser.newContext({
90
+ userAgent:
91
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
92
+ viewport: { width: 1280, height: 800 },
93
+ locale: "en-US",
94
+ timezoneId: "America/Los_Angeles",
95
+ });
96
+
97
+ // Load saved cookies
98
+ await loadCookies(context);
99
+
100
+ page = await context.newPage();
101
+ page.setDefaultTimeout(DEFAULT_TIMEOUT);
102
+ }
103
+
104
+ /**
105
+ * Get the current page, initializing if needed
106
+ */
107
+ async function getPage(): Promise<Page> {
108
+ await initBrowser();
109
+ if (!page) throw new Error("Failed to initialize browser");
110
+ return page;
111
+ }
112
+
113
+ /**
114
+ * Clean up browser resources
115
+ */
116
+ export async function cleanup(): Promise<void> {
117
+ if (context) {
118
+ await saveCookies(context);
119
+ }
120
+ if (browser) {
121
+ await browser.close();
122
+ browser = null;
123
+ context = null;
124
+ page = null;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Get Chase login URL and instructions
130
+ */
131
+ export async function getLoginUrl(): Promise<{ url: string; instructions: string }> {
132
+ return {
133
+ url: "https://secure.chase.com/web/auth/dashboard",
134
+ instructions:
135
+ "Please log in to Chase in a browser, then export your cookies to ~/.strider/chase/cookies.json. Use a browser extension like 'Cookie-Editor' to export cookies in JSON format. Note: Chase uses MFA, so you may need to re-export cookies after each session.",
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Check authentication status
141
+ */
142
+ export async function checkAuth(): Promise<AuthState> {
143
+ try {
144
+ const p = await getPage();
145
+ await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
146
+
147
+ // Check if we're on the dashboard or redirected to login
148
+ const currentUrl = p.url();
149
+
150
+ if (currentUrl.includes("/auth/logon") || currentUrl.includes("/login")) {
151
+ return { isLoggedIn: false };
152
+ }
153
+
154
+ // Try to get account holder name
155
+ const holderName = await p.$eval(
156
+ '.mds-header-user-name, .account-holder-name, [data-testid="user-greeting"]',
157
+ (el) => el.textContent?.trim() || "",
158
+ ).catch(() => "");
159
+
160
+ return {
161
+ isLoggedIn: true,
162
+ accountHolder: holderName || undefined,
163
+ };
164
+ } catch (error) {
165
+ return { isLoggedIn: false };
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Get all accounts
171
+ */
172
+ export async function getAccounts(): Promise<{ success: boolean; accounts?: Account[]; error?: string }> {
173
+ try {
174
+ const p = await getPage();
175
+ await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
176
+
177
+ await p.waitForTimeout(2000);
178
+
179
+ const accounts = await p.$$eval(
180
+ '.account-tile, .account-card, [data-testid="account-tile"]',
181
+ (elements) =>
182
+ elements.map((el, index) => {
183
+ const nameEl = el.querySelector('.account-name, .tile-header, h3');
184
+ const balanceEl = el.querySelector('.account-balance, .balance, [data-testid="balance"]');
185
+ const lastFourEl = el.querySelector('.account-last-four, .masked-number');
186
+ const typeEl = el.querySelector('.account-type');
187
+
188
+ const name = nameEl?.textContent?.trim() || `Account ${index + 1}`;
189
+ const balanceText = balanceEl?.textContent?.trim() || '0';
190
+ const balance = parseFloat(balanceText.replace(/[$,]/g, '')) || 0;
191
+ const lastFour = lastFourEl?.textContent?.trim().replace(/[^0-9]/g, '').slice(-4) || undefined;
192
+
193
+ let type: 'checking' | 'savings' | 'credit' | 'investment' | 'loan' = 'checking';
194
+ const typeText = (typeEl?.textContent || name).toLowerCase();
195
+ if (typeText.includes('saving')) type = 'savings';
196
+ else if (typeText.includes('credit') || typeText.includes('card')) type = 'credit';
197
+ else if (typeText.includes('invest') || typeText.includes('brokerage')) type = 'investment';
198
+ else if (typeText.includes('loan') || typeText.includes('mortgage') || typeText.includes('auto')) type = 'loan';
199
+
200
+ return {
201
+ id: el.getAttribute('data-account-id') || `account-${index}`,
202
+ name,
203
+ type,
204
+ balance,
205
+ lastFour,
206
+ };
207
+ }),
208
+ );
209
+
210
+ return { success: true, accounts };
211
+ } catch (error) {
212
+ return {
213
+ success: false,
214
+ error: error instanceof Error ? error.message : "Failed to get accounts",
215
+ };
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Get account transactions
221
+ */
222
+ export async function getTransactions(
223
+ accountId: string,
224
+ limit: number = 25
225
+ ): Promise<{ success: boolean; transactions?: Transaction[]; error?: string }> {
226
+ try {
227
+ const p = await getPage();
228
+
229
+ // Navigate to account details
230
+ await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
231
+ await p.waitForTimeout(1000);
232
+
233
+ // Click on the account
234
+ const accountTile = await p.$(`[data-account-id="${accountId}"], .account-tile:nth-child(${parseInt(accountId.replace('account-', '')) + 1})`);
235
+ if (accountTile) {
236
+ await accountTile.click();
237
+ await p.waitForNavigation({ waitUntil: "networkidle" }).catch(() => {});
238
+ }
239
+
240
+ await p.waitForTimeout(2000);
241
+
242
+ const transactions = await p.$$eval(
243
+ '.transaction-row, .activity-row, [data-testid="transaction"]',
244
+ (elements, maxItems) =>
245
+ elements.slice(0, maxItems).map((el, index) => {
246
+ const dateEl = el.querySelector('.transaction-date, .date, td:first-child');
247
+ const descEl = el.querySelector('.transaction-description, .description, .merchant-name');
248
+ const amountEl = el.querySelector('.transaction-amount, .amount');
249
+ const pendingEl = el.querySelector('.pending, .pending-badge');
250
+ const categoryEl = el.querySelector('.category, .transaction-category');
251
+
252
+ const amountText = amountEl?.textContent?.trim() || '0';
253
+ const amount = Math.abs(parseFloat(amountText.replace(/[$,]/g, ''))) || 0;
254
+ const isCredit = amountText.includes('+') || el.classList.contains('credit');
255
+
256
+ return {
257
+ id: el.getAttribute('data-transaction-id') || `txn-${index}`,
258
+ date: dateEl?.textContent?.trim() || '',
259
+ description: descEl?.textContent?.trim() || 'Unknown',
260
+ amount,
261
+ type: isCredit ? 'credit' : 'debit' as 'credit' | 'debit',
262
+ category: categoryEl?.textContent?.trim() || undefined,
263
+ pending: !!pendingEl,
264
+ };
265
+ }),
266
+ limit
267
+ );
268
+
269
+ return { success: true, transactions };
270
+ } catch (error) {
271
+ return {
272
+ success: false,
273
+ error: error instanceof Error ? error.message : "Failed to get transactions",
274
+ };
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Get account balance
280
+ */
281
+ export async function getBalance(accountId: string): Promise<{ success: boolean; balance?: number; availableBalance?: number; error?: string }> {
282
+ try {
283
+ const accountsResult = await getAccounts();
284
+ if (!accountsResult.success || !accountsResult.accounts) {
285
+ return { success: false, error: "Failed to get accounts" };
286
+ }
287
+
288
+ const account = accountsResult.accounts.find(a => a.id === accountId);
289
+ if (!account) {
290
+ return { success: false, error: `Account not found: ${accountId}` };
291
+ }
292
+
293
+ return {
294
+ success: true,
295
+ balance: account.balance,
296
+ availableBalance: account.availableBalance || account.balance,
297
+ };
298
+ } catch (error) {
299
+ return {
300
+ success: false,
301
+ error: error instanceof Error ? error.message : "Failed to get balance",
302
+ };
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Get bills/payees
308
+ */
309
+ export async function getBills(): Promise<{ success: boolean; bills?: Bill[]; error?: string }> {
310
+ try {
311
+ const p = await getPage();
312
+ await p.goto(`${CHASE_BASE_URL}/web/auth/billpay`, { waitUntil: "networkidle" });
313
+
314
+ await p.waitForTimeout(2000);
315
+
316
+ const bills = await p.$$eval(
317
+ '.payee-row, .bill-row, [data-testid="payee"]',
318
+ (elements) =>
319
+ elements.map((el, index) => {
320
+ const payeeEl = el.querySelector('.payee-name, .name');
321
+ const amountEl = el.querySelector('.amount, .payment-amount');
322
+ const dueDateEl = el.querySelector('.due-date, .date');
323
+ const statusEl = el.querySelector('.status, .payment-status');
324
+ const autopayEl = el.querySelector('.autopay, .auto-pay-enabled');
325
+
326
+ const amountText = amountEl?.textContent?.trim() || '0';
327
+ const amount = parseFloat(amountText.replace(/[$,]/g, '')) || 0;
328
+
329
+ let status: 'scheduled' | 'paid' | 'overdue' = 'scheduled';
330
+ const statusText = statusEl?.textContent?.toLowerCase() || '';
331
+ if (statusText.includes('paid')) status = 'paid';
332
+ else if (statusText.includes('overdue') || statusText.includes('past due')) status = 'overdue';
333
+
334
+ return {
335
+ id: el.getAttribute('data-payee-id') || `bill-${index}`,
336
+ payee: payeeEl?.textContent?.trim() || 'Unknown Payee',
337
+ amount,
338
+ dueDate: dueDateEl?.textContent?.trim() || '',
339
+ status,
340
+ autopay: !!autopayEl,
341
+ };
342
+ }),
343
+ );
344
+
345
+ return { success: true, bills };
346
+ } catch (error) {
347
+ return {
348
+ success: false,
349
+ error: error instanceof Error ? error.message : "Failed to get bills",
350
+ };
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Get transfer history
356
+ */
357
+ export async function getTransfers(): Promise<{ success: boolean; transfers?: Transfer[]; error?: string }> {
358
+ try {
359
+ const p = await getPage();
360
+ await p.goto(`${CHASE_BASE_URL}/web/auth/transfer`, { waitUntil: "networkidle" });
361
+
362
+ await p.waitForTimeout(2000);
363
+
364
+ const transfers = await p.$$eval(
365
+ '.transfer-row, .activity-row, [data-testid="transfer"]',
366
+ (elements) =>
367
+ elements.slice(0, 20).map((el, index) => {
368
+ const fromEl = el.querySelector('.from-account, .source');
369
+ const toEl = el.querySelector('.to-account, .destination');
370
+ const amountEl = el.querySelector('.amount');
371
+ const dateEl = el.querySelector('.date, .transfer-date');
372
+ const statusEl = el.querySelector('.status');
373
+
374
+ const amountText = amountEl?.textContent?.trim() || '0';
375
+ const amount = parseFloat(amountText.replace(/[$,]/g, '')) || 0;
376
+
377
+ let status: 'pending' | 'completed' | 'failed' = 'completed';
378
+ const statusText = statusEl?.textContent?.toLowerCase() || '';
379
+ if (statusText.includes('pending')) status = 'pending';
380
+ else if (statusText.includes('failed') || statusText.includes('cancelled')) status = 'failed';
381
+
382
+ return {
383
+ id: el.getAttribute('data-transfer-id') || `transfer-${index}`,
384
+ fromAccount: fromEl?.textContent?.trim() || 'Unknown',
385
+ toAccount: toEl?.textContent?.trim() || 'Unknown',
386
+ amount,
387
+ date: dateEl?.textContent?.trim() || '',
388
+ status,
389
+ };
390
+ }),
391
+ );
392
+
393
+ return { success: true, transfers };
394
+ } catch (error) {
395
+ return {
396
+ success: false,
397
+ error: error instanceof Error ? error.message : "Failed to get transfers",
398
+ };
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Get statements
404
+ */
405
+ export async function getStatements(accountId: string): Promise<{ success: boolean; statements?: Statement[]; error?: string }> {
406
+ try {
407
+ const p = await getPage();
408
+
409
+ // Navigate to account and then statements
410
+ await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
411
+ await p.waitForTimeout(1000);
412
+
413
+ // This would need to navigate to the specific account's statements section
414
+ // For now, return a message about navigation
415
+
416
+ const statements = await p.$$eval(
417
+ '.statement-row, [data-testid="statement"]',
418
+ (elements, acctId) =>
419
+ elements.slice(0, 12).map((el, index) => {
420
+ const periodEl = el.querySelector('.period, .statement-period');
421
+ const dateEl = el.querySelector('.date, .statement-date');
422
+
423
+ return {
424
+ id: el.getAttribute('data-statement-id') || `stmt-${index}`,
425
+ accountId: acctId,
426
+ period: periodEl?.textContent?.trim() || '',
427
+ date: dateEl?.textContent?.trim() || '',
428
+ };
429
+ }),
430
+ accountId
431
+ );
432
+
433
+ return { success: true, statements };
434
+ } catch (error) {
435
+ return {
436
+ success: false,
437
+ error: error instanceof Error ? error.message : "Failed to get statements",
438
+ };
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Get rewards/points info
444
+ */
445
+ export async function getRewards(): Promise<{ success: boolean; rewards?: RewardsInfo; error?: string }> {
446
+ try {
447
+ const p = await getPage();
448
+ await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
449
+
450
+ await p.waitForTimeout(2000);
451
+
452
+ const rewards = await p.evaluate(() => {
453
+ const pointsEl = document.querySelector('.points-balance, .ultimate-rewards, [data-testid="points"]');
454
+ const cashBackEl = document.querySelector('.cash-back-balance, [data-testid="cashback"]');
455
+ const tierEl = document.querySelector('.rewards-tier, .membership-level');
456
+
457
+ const pointsText = pointsEl?.textContent?.trim() || '0';
458
+ const cashBackText = cashBackEl?.textContent?.trim() || '0';
459
+
460
+ return {
461
+ pointsBalance: parseInt(pointsText.replace(/[^0-9]/g, '')) || 0,
462
+ cashBackBalance: parseFloat(cashBackText.replace(/[$,]/g, '')) || 0,
463
+ pendingRewards: 0,
464
+ tier: tierEl?.textContent?.trim() || undefined,
465
+ };
466
+ });
467
+
468
+ return { success: true, rewards };
469
+ } catch (error) {
470
+ return {
471
+ success: false,
472
+ error: error instanceof Error ? error.message : "Failed to get rewards",
473
+ };
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Initiate a transfer (preview only - does not execute)
479
+ */
480
+ export async function initiateTransfer(
481
+ fromAccountId: string,
482
+ toAccountId: string,
483
+ amount: number
484
+ ): Promise<{ success: boolean; preview?: object; error?: string }> {
485
+ // For security, this only provides a preview and instructions
486
+ // The actual transfer would need to be confirmed by the user
487
+ return {
488
+ success: true,
489
+ preview: {
490
+ fromAccountId,
491
+ toAccountId,
492
+ amount,
493
+ message: "For security reasons, transfers must be completed manually in Chase. Navigate to Transfers in your Chase account to complete this transfer.",
494
+ instructions: [
495
+ "1. Log in to Chase.com or Chase mobile app",
496
+ "2. Go to 'Pay & Transfer' > 'Transfers'",
497
+ "3. Select your from and to accounts",
498
+ `4. Enter amount: $${amount.toFixed(2)}`,
499
+ "5. Review and confirm the transfer"
500
+ ]
501
+ }
502
+ };
503
+ }
504
+
505
+ /**
506
+ * Pay a bill (preview only - does not execute)
507
+ */
508
+ export async function payBill(
509
+ payeeId: string,
510
+ amount: number,
511
+ date?: string
512
+ ): Promise<{ success: boolean; preview?: object; error?: string }> {
513
+ // For security, this only provides a preview and instructions
514
+ return {
515
+ success: true,
516
+ preview: {
517
+ payeeId,
518
+ amount,
519
+ scheduledDate: date || "Immediately",
520
+ message: "For security reasons, bill payments must be completed manually in Chase.",
521
+ instructions: [
522
+ "1. Log in to Chase.com or Chase mobile app",
523
+ "2. Go to 'Pay & Transfer' > 'Pay Bills'",
524
+ "3. Select the payee",
525
+ `4. Enter amount: $${amount.toFixed(2)}`,
526
+ "5. Choose payment date and confirm"
527
+ ]
528
+ }
529
+ };
530
+ }