monarch-money-api 0.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/index2.js ADDED
@@ -0,0 +1,545 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import fetch from 'node-fetch';
4
+ import { GraphQLClient, gql } from 'graphql-request';
5
+ import FormData from 'form-data';
6
+ import readline from 'readline';
7
+ import { promisify } from 'util';
8
+
9
+ // Constants
10
+ const AUTH_HEADER_KEY = "authorization";
11
+ const CSRF_KEY = "csrftoken";
12
+ const DEFAULT_RECORD_LIMIT = 100;
13
+ const ERRORS_KEY = "error_code";
14
+ const SESSION_DIR = ".mm";
15
+ const SESSION_FILE = path.join(SESSION_DIR, "mm_session.json");
16
+
17
+ // Helper function to create readline interface for interactive login
18
+ const createInterface = () => readline.createInterface({
19
+ input: process.stdin,
20
+ output: process.stdout
21
+ });
22
+
23
+ // Monarch Money Endpoints
24
+ const MonarchMoneyEndpoints = {
25
+ BASE_URL: "https://api.monarchmoney.com",
26
+
27
+ getLoginEndpoint() {
28
+ return `${this.BASE_URL}/auth/login/`;
29
+ },
30
+
31
+ getGraphQL() {
32
+ return `${this.BASE_URL}/graphql`;
33
+ },
34
+
35
+ getAccountBalanceHistoryUploadEndpoint() {
36
+ return `${this.BASE_URL}/account-balance-history/upload/`;
37
+ }
38
+ };
39
+
40
+ // Custom Exceptions
41
+ class RequireMFAException extends Error {}
42
+ class LoginFailedException extends Error {}
43
+ class RequestFailedException extends Error {}
44
+
45
+ // Session Management
46
+ let headers = {
47
+ "Client-Platform": "web",
48
+ };
49
+ let token = null;
50
+ let timeout = 10;
51
+ let sessionFile = SESSION_FILE;
52
+
53
+ const setToken = (newToken) => {
54
+ token = newToken;
55
+ headers["Authorization"] = `Token ${newToken}`;
56
+ };
57
+
58
+ const saveSession = (filename = sessionFile) => {
59
+ filename = path.resolve(filename);
60
+ const sessionData = { token };
61
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
62
+ fs.writeFileSync(filename, JSON.stringify(sessionData));
63
+ };
64
+
65
+ const loadSession = (filename = sessionFile) => {
66
+ const data = JSON.parse(fs.readFileSync(filename, 'utf-8'));
67
+ setToken(data.token);
68
+ };
69
+
70
+ const deleteSession = (filename = sessionFile) => {
71
+ if (fs.existsSync(filename)) {
72
+ fs.unlinkSync(filename);
73
+ }
74
+ };
75
+
76
+ // GraphQL Client
77
+ const getGraphQLClient = () => {
78
+ if (!headers["Authorization"]) {
79
+ throw new LoginFailedException("Make sure you call login() first or provide a session token!");
80
+ }
81
+ return new GraphQLClient(MonarchMoneyEndpoints.getGraphQL(), {
82
+ headers,
83
+ timeout: timeout * 1000,
84
+ });
85
+ };
86
+
87
+ // API Calls
88
+ const gqlCall = async (operation, graphqlQuery, variables = {}) => {
89
+ const client = getGraphQLClient();
90
+ return await client.request(graphqlQuery, variables);
91
+ };
92
+
93
+ // User Interaction
94
+ const interactiveLogin = async (useSavedSession = true, saveSessionFlag = true) => {
95
+ const rl = createInterface();
96
+ const email = await promisify(rl.question).bind(rl)("Email: ");
97
+ const passwd = await promisify(rl.question).bind(rl)("Password: ");
98
+ rl.close();
99
+
100
+ try {
101
+ await login(email, passwd, useSavedSession, saveSessionFlag);
102
+ } catch (error) {
103
+ if (error instanceof RequireMFAException) {
104
+ const rl = createInterface();
105
+ const twoFactorCode = await promisify(rl.question).bind(rl)("Two Factor Code: ");
106
+ rl.close();
107
+ await multiFactorAuthenticate(email, passwd, twoFactorCode);
108
+ if (saveSessionFlag) {
109
+ saveSession(sessionFile);
110
+ }
111
+ } else {
112
+ throw error;
113
+ }
114
+ }
115
+ };
116
+
117
+ const login = async (email = null, password = null, useSavedSession = true, saveSessionFlag = true, mfaSecretKey = null) => {
118
+ if (useSavedSession && fs.existsSync(sessionFile)) {
119
+ console.log(`Using saved session found at ${sessionFile}`);
120
+ loadSession(sessionFile);
121
+ return;
122
+ }
123
+
124
+ if (!email || !password) {
125
+ throw new LoginFailedException("Email and password are required to login when not using a saved session.");
126
+ }
127
+ await loginUser(email, password, mfaSecretKey);
128
+ if (saveSessionFlag) {
129
+ saveSession(sessionFile);
130
+ }
131
+ };
132
+
133
+ const loginUser = async (email, password, mfaSecretKey) => {
134
+ const data = new URLSearchParams({
135
+ password,
136
+ supports_mfa: true,
137
+ trusted_device: false,
138
+ username: email,
139
+ });
140
+
141
+ if (mfaSecretKey) {
142
+ data.append("totp", generateOtp(mfaSecretKey));
143
+ }
144
+
145
+ const response = await fetch(MonarchMoneyEndpoints.getLoginEndpoint(), {
146
+ method: 'POST',
147
+ headers,
148
+ body: data,
149
+ });
150
+
151
+ if (response.status === 403) {
152
+ throw new RequireMFAException("Multi-Factor Auth Required");
153
+ } else if (response.status !== 200) {
154
+ throw new LoginFailedException(`HTTP Code ${response.status}: ${response.statusText}`);
155
+ }
156
+
157
+ const json = await response.json();
158
+ setToken(json.token);
159
+ };
160
+
161
+ const multiFactorAuthenticate = async (email, password, code) => {
162
+ const data = new URLSearchParams({
163
+ password,
164
+ supports_mfa: true,
165
+ totp: code,
166
+ trusted_device: false,
167
+ username: email,
168
+ });
169
+
170
+ const response = await fetch(MonarchMoneyEndpoints.getLoginEndpoint(), {
171
+ method: 'POST',
172
+ headers,
173
+ body: data,
174
+ });
175
+
176
+ if (response.status !== 200) {
177
+ const json = await response.json();
178
+ const errorMessage = json[ERRORS_KEY] ? json[ERRORS_KEY] : "Unknown error";
179
+ throw new LoginFailedException(errorMessage);
180
+ }
181
+
182
+ const json = await response.json();
183
+ setToken(json.token);
184
+ };
185
+
186
+ // Sample API Method
187
+ const getAccounts = async () => {
188
+ const query = gql`
189
+ query GetAccounts {
190
+ accounts {
191
+ ...AccountFields
192
+ __typename
193
+ }
194
+ householdPreferences {
195
+ id
196
+ accountGroupOrder
197
+ __typename
198
+ }
199
+ }
200
+ fragment AccountFields on Account {
201
+ id
202
+ displayName
203
+ syncDisabled
204
+ deactivatedAt
205
+ isHidden
206
+ isAsset
207
+ mask
208
+ createdAt
209
+ updatedAt
210
+ displayLastUpdatedAt
211
+ currentBalance
212
+ displayBalance
213
+ includeInNetWorth
214
+ hideFromList
215
+ hideTransactionsFromReports
216
+ includeBalanceInNetWorth
217
+ includeInGoalBalance
218
+ dataProvider
219
+ dataProviderAccountId
220
+ isManual
221
+ transactionsCount
222
+ holdingsCount
223
+ manualInvestmentsTrackingMethod
224
+ order
225
+ logoUrl
226
+ type {
227
+ name
228
+ display
229
+ __typename
230
+ }
231
+ subtype {
232
+ name
233
+ display
234
+ __typename
235
+ }
236
+ credential {
237
+ id
238
+ updateRequired
239
+ disconnectedFromDataProviderAt
240
+ dataProvider
241
+ institution {
242
+ id
243
+ plaidInstitutionId
244
+ name
245
+ status
246
+ __typename
247
+ }
248
+ __typename
249
+ }
250
+ institution {
251
+ id
252
+ name
253
+ primaryColor
254
+ url
255
+ __typename
256
+ }
257
+ __typename
258
+ }
259
+ `;
260
+ return await gqlCall("GetAccounts", query);
261
+ };
262
+
263
+ // Additional functions translated here...
264
+ export const getAccountTypeOptions = async () => {
265
+ const query = gql`
266
+ query GetAccountTypeOptions {
267
+ accountTypeOptions {
268
+ type {
269
+ name
270
+ display
271
+ group
272
+ possibleSubtypes {
273
+ display
274
+ name
275
+ __typename
276
+ }
277
+ __typename
278
+ }
279
+ subtype {
280
+ name
281
+ display
282
+ __typename
283
+ }
284
+ __typename
285
+ }
286
+ }
287
+ `;
288
+ return await gqlCall("GetAccountTypeOptions", query);
289
+ };
290
+
291
+ export const getRecentAccountBalances = async (startDate = null) => {
292
+ if (!startDate) {
293
+ startDate = new Date();
294
+ startDate.setDate(startDate.getDate() - 31);
295
+ startDate = startDate.toISOString().split('T')[0];
296
+ }
297
+
298
+ const query = gql`
299
+ query GetAccountRecentBalances($startDate: Date!) {
300
+ accounts {
301
+ id
302
+ recentBalances(startDate: $startDate)
303
+ __typename
304
+ }
305
+ }
306
+ `;
307
+ return await gqlCall("GetAccountRecentBalances", query, { startDate });
308
+ };
309
+
310
+ export const getAccountSnapshotsByType = async (startDate, timeframe) => {
311
+ if (!["year", "month"].includes(timeframe)) {
312
+ throw new Error(`Unknown timeframe "${timeframe}"`);
313
+ }
314
+
315
+ const query = gql`
316
+ query GetSnapshotsByAccountType($startDate: Date!, $timeframe: Timeframe!) {
317
+ snapshotsByAccountType(startDate: $startDate, timeframe: $timeframe) {
318
+ accountType
319
+ month
320
+ balance
321
+ __typename
322
+ }
323
+ accountTypes {
324
+ name
325
+ group
326
+ __typename
327
+ }
328
+ }
329
+ `;
330
+ return await gqlCall("GetSnapshotsByAccountType", query, { startDate, timeframe });
331
+ };
332
+
333
+ export const getAggregateSnapshots = async (startDate = null, endDate = null, accountType = null) => {
334
+ if (!startDate) {
335
+ startDate = new Date();
336
+ startDate.setFullYear(startDate.getFullYear() - 150);
337
+ startDate.setDate(1);
338
+ startDate = startDate.toISOString().split('T')[0];
339
+ }
340
+
341
+ const query = gql`
342
+ query GetAggregateSnapshots($filters: AggregateSnapshotFilters) {
343
+ aggregateSnapshots(filters: $filters) {
344
+ date
345
+ balance
346
+ __typename
347
+ }
348
+ }
349
+ `;
350
+
351
+ const variables = {
352
+ filters: {
353
+ startDate,
354
+ endDate,
355
+ accountType,
356
+ },
357
+ };
358
+
359
+ return await gqlCall("GetAggregateSnapshots", query, variables);
360
+ };
361
+
362
+ export const createManualAccount = async (accountType, accountSubType, isInNetWorth, accountName, accountBalance = 0) => {
363
+ const query = gql`
364
+ mutation Web_CreateManualAccount($input: CreateManualAccountMutationInput!) {
365
+ createManualAccount(input: $input) {
366
+ account {
367
+ id
368
+ __typename
369
+ }
370
+ errors {
371
+ ...PayloadErrorFields
372
+ __typename
373
+ }
374
+ __typename
375
+ }
376
+ }
377
+ fragment PayloadErrorFields on PayloadError {
378
+ fieldErrors {
379
+ field
380
+ messages
381
+ __typename
382
+ }
383
+ message
384
+ code
385
+ __typename
386
+ }
387
+ `;
388
+
389
+ const variables = {
390
+ input: {
391
+ type: accountType,
392
+ subtype: accountSubType,
393
+ includeInNetWorth: isInNetWorth,
394
+ name: accountName,
395
+ displayBalance: accountBalance,
396
+ },
397
+ };
398
+
399
+ return await gqlCall("Web_CreateManualAccount", query, variables);
400
+ };
401
+
402
+ export const updateAccount = async (accountId, accountName = null, accountBalance = null, accountType = null, accountSubType = null, includeInNetWorth = null, hideFromSummaryList = null, hideTransactionsFromReports = null) => {
403
+ const query = gql`
404
+ mutation Common_UpdateAccount($input: UpdateAccountMutationInput!) {
405
+ updateAccount(input: $input) {
406
+ account {
407
+ ...AccountFields
408
+ __typename
409
+ }
410
+ errors {
411
+ ...PayloadErrorFields
412
+ __typename
413
+ }
414
+ __typename
415
+ }
416
+ }
417
+ fragment AccountFields on Account {
418
+ id
419
+ displayName
420
+ syncDisabled
421
+ deactivatedAt
422
+ isHidden
423
+ isAsset
424
+ mask
425
+ createdAt
426
+ updatedAt
427
+ displayLastUpdatedAt
428
+ currentBalance
429
+ displayBalance
430
+ includeInNetWorth
431
+ hideFromList
432
+ hideTransactionsFromReports
433
+ includeBalanceInNetWorth
434
+ includeInGoalBalance
435
+ dataProvider
436
+ dataProviderAccountId
437
+ isManual
438
+ transactionsCount
439
+ holdingsCount
440
+ manualInvestmentsTrackingMethod
441
+ order
442
+ icon
443
+ logoUrl
444
+ deactivatedAt
445
+ type {
446
+ name
447
+ display
448
+ group
449
+ __typename
450
+ }
451
+ subtype {
452
+ name
453
+ display
454
+ __typename
455
+ }
456
+ credential {
457
+ id
458
+ updateRequired
459
+ disconnectedFromDataProviderAt
460
+ dataProvider
461
+ institution {
462
+ id
463
+ plaidInstitutionId
464
+ name
465
+ status
466
+ __typename
467
+ }
468
+ __typename
469
+ }
470
+ institution {
471
+ id
472
+ name
473
+ primaryColor
474
+ url
475
+ __typename
476
+ }
477
+ __typename
478
+ }
479
+ fragment PayloadErrorFields on PayloadError {
480
+ fieldErrors {
481
+ field
482
+ messages
483
+ __typename
484
+ }
485
+ message
486
+ code
487
+ __typename
488
+ }
489
+ `;
490
+
491
+ const variables = {
492
+ input: {
493
+ id: accountId,
494
+ name: accountName,
495
+ type: accountType,
496
+ subtype: accountSubType,
497
+ includeInNetWorth,
498
+ hideFromList: hideFromSummaryList,
499
+ hideTransactionsFromReports,
500
+ displayBalance: accountBalance,
501
+ },
502
+ };
503
+
504
+ return await gqlCall("Common_UpdateAccount", query, variables);
505
+ };
506
+
507
+ export const deleteAccount = async (accountId) => {
508
+ const query = gql`
509
+ mutation Common_DeleteAccount($id: UUID!) {
510
+ deleteAccount(id: $id) {
511
+ deleted
512
+ errors {
513
+ ...PayloadErrorFields
514
+ __typename
515
+ }
516
+ __typename
517
+ }
518
+ }
519
+ fragment PayloadErrorFields on PayloadError {
520
+ fieldErrors {
521
+ field
522
+ messages
523
+ __typename
524
+ }
525
+ message
526
+ code
527
+ __typename
528
+ }
529
+ `;
530
+
531
+ const variables = { id: accountId };
532
+
533
+ return await gqlCall("Common_DeleteAccount", query, variables);
534
+ };
535
+
536
+ // Example of how you might use these functions
537
+ (async () => {
538
+ try {
539
+ await interactiveLogin();
540
+ const accounts = await getAccounts();
541
+ console.log(accounts);
542
+ } catch (err) {
543
+ console.error(err);
544
+ }
545
+ })();