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/LICENSE +21 -0
- package/README.md +156 -0
- package/graphql.config.cjs +14 -0
- package/index.js +2169 -0
- package/index2.js +545 -0
- package/monarchmoney.py +2884 -0
- package/package.json +22 -0
- package/src/api.js +2105 -0
- package/src/constants.js +32 -0
- package/src/index.js +4 -0
- package/src/login.js +102 -0
- package/src/session.js +48 -0
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
|
+
})();
|