@actual-app/core 26.3.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.
Files changed (358) hide show
  1. package/.swcrc +11 -0
  2. package/bin/build-browser +40 -0
  3. package/bin/copy-migrations +9 -0
  4. package/db.sqlite +0 -0
  5. package/default-db.sqlite +0 -0
  6. package/migrations/.force-copy-windows +0 -0
  7. package/migrations/1548957970627_remove-db-version.sql +5 -0
  8. package/migrations/1550601598648_payees.sql +23 -0
  9. package/migrations/1555786194328_remove_category_group_unique.sql +25 -0
  10. package/migrations/1561751833510_indexes.sql +7 -0
  11. package/migrations/1567699552727_budget.sql +38 -0
  12. package/migrations/1582384163573_cleared.sql +6 -0
  13. package/migrations/1597756566448_rules.sql +10 -0
  14. package/migrations/1608652596043_parent_field.sql +13 -0
  15. package/migrations/1608652596044_trans_views.sql +56 -0
  16. package/migrations/1612625548236_optimize.sql +7 -0
  17. package/migrations/1614782639336_trans_views2.sql +33 -0
  18. package/migrations/1615745967948_meta.sql +10 -0
  19. package/migrations/1616167010796_accounts_order.sql +5 -0
  20. package/migrations/1618975177358_schedules.sql +28 -0
  21. package/migrations/1632571489012_remove_cache.js +136 -0
  22. package/migrations/1679728867040_rules_conditions.sql +5 -0
  23. package/migrations/1681115033845_add_schedule_name.sql +5 -0
  24. package/migrations/1682974838138_remove_payee_rules.sql +5 -0
  25. package/migrations/1685007876842_add_category_hidden.sql +6 -0
  26. package/migrations/1686139660866_remove_account_type.sql +5 -0
  27. package/migrations/1688749527273_transaction_filters.sql +10 -0
  28. package/migrations/1688841238000_add_account_type.sql +5 -0
  29. package/migrations/1691233396000_add_schedule_next_date_tombstone.sql +5 -0
  30. package/migrations/1694438752000_add_goal_targets.sql +7 -0
  31. package/migrations/1697046240000_add_reconciled.sql +5 -0
  32. package/migrations/1704572023730_add_account_sync_source.sql +5 -0
  33. package/migrations/1704572023731_add_missing_goCardless_sync_source.sql +9 -0
  34. package/migrations/1707267033000_reports.sql +28 -0
  35. package/migrations/1712784523000_unhide_input_group.sql +8 -0
  36. package/migrations/1716359441000_include_current.sql +5 -0
  37. package/migrations/1720310586000_link_transfer_schedules.sql +19 -0
  38. package/migrations/1720664867241_add_payee_favorite.sql +5 -0
  39. package/migrations/1720665000000_goal_context.sql +6 -0
  40. package/migrations/1722717601000_reports_move_selected_categories.js +55 -0
  41. package/migrations/1722804019000_create_dashboard_table.js +69 -0
  42. package/migrations/1723665565000_prefs.js +59 -0
  43. package/migrations/1730744182000_fix_dashboard_table.sql +7 -0
  44. package/migrations/1736640000000_custom_report_sorting.sql +7 -0
  45. package/migrations/1737158400000_add_learn_categories_to_payees.sql +5 -0
  46. package/migrations/1738491452000_sorting_rename.sql +13 -0
  47. package/migrations/1739139550000_bank_sync_page.sql +7 -0
  48. package/migrations/1740506588539_add_last_reconciled_at.sql +5 -0
  49. package/migrations/1745425408000_update_budgetType_pref.sql +7 -0
  50. package/migrations/1749799110000_add_tags.sql +10 -0
  51. package/migrations/1749799110001_tags_tombstone.sql +5 -0
  52. package/migrations/1754611200000_add_category_template_settings.sql +5 -0
  53. package/migrations/1759260219000_add_trim_interval_report_setting.sql +6 -0
  54. package/migrations/1759842823172_add_isGlobal_to_preferences.sql +1 -0
  55. package/migrations/1762178745667_rename_csv_skip_lines_pref.sql +8 -0
  56. package/migrations/1765518577215_multiple_dashboards.js +30 -0
  57. package/migrations/1768872504000_add_payee_locations.sql +21 -0
  58. package/package.json +128 -0
  59. package/src/mocks/arbitrary-schema.ts +162 -0
  60. package/src/mocks/budget.ts +901 -0
  61. package/src/mocks/files/8859-1.qfx +63 -0
  62. package/src/mocks/files/best.data-ever$.QFX +124 -0
  63. package/src/mocks/files/big.data.QiF +91 -0
  64. package/src/mocks/files/budgets/.commit-to-git +0 -0
  65. package/src/mocks/files/camt/camt.053.payee-memo.xml +127 -0
  66. package/src/mocks/files/camt/camt.053.xml +463 -0
  67. package/src/mocks/files/credit-card.ofx +11 -0
  68. package/src/mocks/files/data-multi-decimal.ofx +64 -0
  69. package/src/mocks/files/data-payee-memo.ofx +75 -0
  70. package/src/mocks/files/data-payee-memo.qif +17 -0
  71. package/src/mocks/files/data.ofx +124 -0
  72. package/src/mocks/files/data.qfx +124 -0
  73. package/src/mocks/files/data.qif +91 -0
  74. package/src/mocks/files/default-budget-template/db.sqlite +0 -0
  75. package/src/mocks/files/default-budget-template/metadata.json +6 -0
  76. package/src/mocks/files/html-vals.qfx +17 -0
  77. package/src/mocks/index.ts +221 -0
  78. package/src/mocks/migrations/1508717984291_up_add-poop.sql +13 -0
  79. package/src/mocks/migrations/1508718036311_up_modify-poop.sql +2 -0
  80. package/src/mocks/migrations/1508727787513_remove-is_income.sql +15 -0
  81. package/src/mocks/random.ts +16 -0
  82. package/src/mocks/setup.ts +180 -0
  83. package/src/mocks/spreadsheet.ts +101 -0
  84. package/src/mocks/util.ts +82 -0
  85. package/src/platform/client/connection/README.md +3 -0
  86. package/src/platform/client/connection/__mocks__/index.ts +67 -0
  87. package/src/platform/client/connection/index-types.ts +95 -0
  88. package/src/platform/client/connection/index.browser.ts +213 -0
  89. package/src/platform/client/connection/index.ts +155 -0
  90. package/src/platform/client/undo/index.ts +59 -0
  91. package/src/platform/exceptions/__mocks__/index.ts +7 -0
  92. package/src/platform/exceptions/index.ts +9 -0
  93. package/src/platform/server/asyncStorage/__mocks__/index.ts +50 -0
  94. package/src/platform/server/asyncStorage/index-types.ts +35 -0
  95. package/src/platform/server/asyncStorage/index.api.ts +2 -0
  96. package/src/platform/server/asyncStorage/index.electron.ts +88 -0
  97. package/src/platform/server/asyncStorage/index.ts +126 -0
  98. package/src/platform/server/connection/README.md +3 -0
  99. package/src/platform/server/connection/__mocks__/index.ts +15 -0
  100. package/src/platform/server/connection/index-types.ts +20 -0
  101. package/src/platform/server/connection/index.api.ts +13 -0
  102. package/src/platform/server/connection/index.electron.ts +102 -0
  103. package/src/platform/server/connection/index.ts +154 -0
  104. package/src/platform/server/fetch/__mocks__/index.ts +3 -0
  105. package/src/platform/server/fetch/index.api.ts +1 -0
  106. package/src/platform/server/fetch/index.electron.ts +18 -0
  107. package/src/platform/server/fetch/index.ts +20 -0
  108. package/src/platform/server/fs/index.api.ts +198 -0
  109. package/src/platform/server/fs/index.electron.ts +208 -0
  110. package/src/platform/server/fs/index.test.ts +117 -0
  111. package/src/platform/server/fs/index.ts +416 -0
  112. package/src/platform/server/fs/path-join.api.ts +1 -0
  113. package/src/platform/server/fs/path-join.electron.ts +1 -0
  114. package/src/platform/server/fs/path-join.ts +97 -0
  115. package/src/platform/server/fs/shared.ts +33 -0
  116. package/src/platform/server/indexeddb/index.ts +115 -0
  117. package/src/platform/server/log/index.ts +43 -0
  118. package/src/platform/server/sqlite/index.api.ts +2 -0
  119. package/src/platform/server/sqlite/index.electron.ts +134 -0
  120. package/src/platform/server/sqlite/index.test.ts +108 -0
  121. package/src/platform/server/sqlite/index.ts +241 -0
  122. package/src/platform/server/sqlite/normalise.ts +9 -0
  123. package/src/platform/server/sqlite/unicodeLike.test.ts +58 -0
  124. package/src/platform/server/sqlite/unicodeLike.ts +31 -0
  125. package/src/server/__mocks__/post.ts +9 -0
  126. package/src/server/__snapshots__/main.test.ts.snap +199 -0
  127. package/src/server/__snapshots__/sheet.test.ts.snap +9 -0
  128. package/src/server/accounts/__snapshots__/sync.test.ts.snap +136 -0
  129. package/src/server/accounts/app-bank-sync.test.ts +136 -0
  130. package/src/server/accounts/app.ts +1294 -0
  131. package/src/server/accounts/link.ts +25 -0
  132. package/src/server/accounts/payees.ts +36 -0
  133. package/src/server/accounts/sync.test.ts +679 -0
  134. package/src/server/accounts/sync.ts +1168 -0
  135. package/src/server/accounts/title/index.ts +60 -0
  136. package/src/server/accounts/title/lower-case.ts +93 -0
  137. package/src/server/accounts/title/specials.ts +21 -0
  138. package/src/server/admin/app.ts +241 -0
  139. package/src/server/api-models.ts +244 -0
  140. package/src/server/api.test.ts +36 -0
  141. package/src/server/api.ts +1030 -0
  142. package/src/server/app.ts +91 -0
  143. package/src/server/aql/compiler.test.ts +966 -0
  144. package/src/server/aql/compiler.ts +1222 -0
  145. package/src/server/aql/exec.test.ts +289 -0
  146. package/src/server/aql/exec.ts +128 -0
  147. package/src/server/aql/index.ts +41 -0
  148. package/src/server/aql/schema/executors.test.ts +420 -0
  149. package/src/server/aql/schema/executors.ts +345 -0
  150. package/src/server/aql/schema/index.test.ts +67 -0
  151. package/src/server/aql/schema/index.ts +409 -0
  152. package/src/server/aql/schema-helpers.test.ts +242 -0
  153. package/src/server/aql/schema-helpers.ts +208 -0
  154. package/src/server/aql/views.test.ts +62 -0
  155. package/src/server/aql/views.ts +57 -0
  156. package/src/server/auth/app.ts +387 -0
  157. package/src/server/bench.ts +29 -0
  158. package/src/server/budget/actions.ts +686 -0
  159. package/src/server/budget/app.ts +469 -0
  160. package/src/server/budget/base.test.ts +340 -0
  161. package/src/server/budget/base.ts +339 -0
  162. package/src/server/budget/category-template-context.test.ts +1658 -0
  163. package/src/server/budget/category-template-context.ts +862 -0
  164. package/src/server/budget/cleanup-template.pegjs +27 -0
  165. package/src/server/budget/cleanup-template.ts +408 -0
  166. package/src/server/budget/envelope.ts +403 -0
  167. package/src/server/budget/goal-template.pegjs +110 -0
  168. package/src/server/budget/goal-template.ts +309 -0
  169. package/src/server/budget/report.ts +308 -0
  170. package/src/server/budget/schedule-template.test.ts +184 -0
  171. package/src/server/budget/schedule-template.ts +351 -0
  172. package/src/server/budget/statements.ts +60 -0
  173. package/src/server/budget/template-notes.test.ts +393 -0
  174. package/src/server/budget/template-notes.ts +323 -0
  175. package/src/server/budget/util.ts +25 -0
  176. package/src/server/budgetfiles/__snapshots__/backups.test.ts.snap +101 -0
  177. package/src/server/budgetfiles/app.ts +672 -0
  178. package/src/server/budgetfiles/backups.test.ts +79 -0
  179. package/src/server/budgetfiles/backups.ts +251 -0
  180. package/src/server/cloud-storage.ts +467 -0
  181. package/src/server/dashboard/app.ts +373 -0
  182. package/src/server/db/__snapshots__/index.test.ts.snap +271 -0
  183. package/src/server/db/index.test.ts +300 -0
  184. package/src/server/db/index.ts +855 -0
  185. package/src/server/db/mappings.ts +59 -0
  186. package/src/server/db/sort.ts +58 -0
  187. package/src/server/db/types/index.ts +342 -0
  188. package/src/server/db/util.ts +36 -0
  189. package/src/server/encryption/app.ts +133 -0
  190. package/src/server/encryption/encryption-internals.api.ts +2 -0
  191. package/src/server/encryption/encryption-internals.electron.ts +89 -0
  192. package/src/server/encryption/encryption-internals.ts +109 -0
  193. package/src/server/encryption/encryption.test.ts +19 -0
  194. package/src/server/encryption/index.test.ts +19 -0
  195. package/src/server/encryption/index.ts +89 -0
  196. package/src/server/errors.ts +110 -0
  197. package/src/server/filters/app.ts +191 -0
  198. package/src/server/importers/actual.ts +49 -0
  199. package/src/server/importers/index.ts +58 -0
  200. package/src/server/importers/ynab4-types.ts +163 -0
  201. package/src/server/importers/ynab4.ts +470 -0
  202. package/src/server/importers/ynab5-types.ts +290 -0
  203. package/src/server/importers/ynab5.ts +1193 -0
  204. package/src/server/main-app.ts +25 -0
  205. package/src/server/main.test.ts +392 -0
  206. package/src/server/main.ts +336 -0
  207. package/src/server/migrate/__snapshots__/migrations.test.ts.snap +17 -0
  208. package/src/server/migrate/cli.ts +100 -0
  209. package/src/server/migrate/migrations.test.ts +81 -0
  210. package/src/server/migrate/migrations.ts +192 -0
  211. package/src/server/models.ts +184 -0
  212. package/src/server/mutators.ts +139 -0
  213. package/src/server/notes/app.ts +18 -0
  214. package/src/server/payees/app.ts +351 -0
  215. package/src/server/polyfills.ts +26 -0
  216. package/src/server/post.ts +219 -0
  217. package/src/server/preferences/app.ts +249 -0
  218. package/src/server/prefs.ts +91 -0
  219. package/src/server/reports/app.ts +187 -0
  220. package/src/server/rules/action.ts +344 -0
  221. package/src/server/rules/app.ts +193 -0
  222. package/src/server/rules/condition.ts +436 -0
  223. package/src/server/rules/customFunctions.ts +61 -0
  224. package/src/server/rules/formula-action.test.ts +175 -0
  225. package/src/server/rules/handlebars-helpers.ts +131 -0
  226. package/src/server/rules/index.test.ts +1095 -0
  227. package/src/server/rules/index.ts +22 -0
  228. package/src/server/rules/rule-indexer.ts +89 -0
  229. package/src/server/rules/rule-utils.ts +274 -0
  230. package/src/server/rules/rule.ts +193 -0
  231. package/src/server/schedules/app.test.ts +502 -0
  232. package/src/server/schedules/app.ts +644 -0
  233. package/src/server/schedules/find-schedules.ts +391 -0
  234. package/src/server/server-config.ts +59 -0
  235. package/src/server/sheet.test.ts +101 -0
  236. package/src/server/sheet.ts +280 -0
  237. package/src/server/spreadsheet/__snapshots__/spreadsheet.test.ts.snap +5 -0
  238. package/src/server/spreadsheet/app.ts +54 -0
  239. package/src/server/spreadsheet/globals.ts +13 -0
  240. package/src/server/spreadsheet/graph-data-structure.ts +165 -0
  241. package/src/server/spreadsheet/scratch +60 -0
  242. package/src/server/spreadsheet/spreadsheet.test.ts +191 -0
  243. package/src/server/spreadsheet/spreadsheet.ts +523 -0
  244. package/src/server/spreadsheet/util.ts +15 -0
  245. package/src/server/sql/init.sql +88 -0
  246. package/src/server/sync/__snapshots__/sync.test.ts.snap +31 -0
  247. package/src/server/sync/app.ts +29 -0
  248. package/src/server/sync/encoder.ts +129 -0
  249. package/src/server/sync/index.ts +820 -0
  250. package/src/server/sync/make-test-message.ts +19 -0
  251. package/src/server/sync/migrate.test.ts +169 -0
  252. package/src/server/sync/migrate.ts +48 -0
  253. package/src/server/sync/repair.ts +39 -0
  254. package/src/server/sync/reset.ts +91 -0
  255. package/src/server/sync/sync.property.test.ts +385 -0
  256. package/src/server/sync/sync.test.ts +349 -0
  257. package/src/server/sync/utils.ts +3 -0
  258. package/src/server/tags/app.ts +101 -0
  259. package/src/server/tests/mockData.json +9352 -0
  260. package/src/server/tests/mockSyncServer.ts +119 -0
  261. package/src/server/tools/app.ts +152 -0
  262. package/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap +173 -0
  263. package/src/server/transactions/__snapshots__/transfer.test.ts.snap +655 -0
  264. package/src/server/transactions/app.ts +136 -0
  265. package/src/server/transactions/export/export-to-csv.ts +132 -0
  266. package/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap +1582 -0
  267. package/src/server/transactions/import/ofx2json.test.ts +33 -0
  268. package/src/server/transactions/import/ofx2json.ts +157 -0
  269. package/src/server/transactions/import/parse-file.test.ts +224 -0
  270. package/src/server/transactions/import/parse-file.ts +286 -0
  271. package/src/server/transactions/import/qif2json.ts +110 -0
  272. package/src/server/transactions/import/xmlcamt2json.ts +168 -0
  273. package/src/server/transactions/index.ts +196 -0
  274. package/src/server/transactions/merge.test.ts +370 -0
  275. package/src/server/transactions/merge.ts +139 -0
  276. package/src/server/transactions/transaction-rules.test.ts +994 -0
  277. package/src/server/transactions/transaction-rules.ts +1038 -0
  278. package/src/server/transactions/transfer.test.ts +221 -0
  279. package/src/server/transactions/transfer.ts +173 -0
  280. package/src/server/undo.ts +271 -0
  281. package/src/server/update.ts +37 -0
  282. package/src/server/util/budget-name.ts +61 -0
  283. package/src/server/util/custom-sync-mapping.ts +48 -0
  284. package/src/server/util/rschedule.ts +9 -0
  285. package/src/shared/__mocks__/platform.ts +7 -0
  286. package/src/shared/__snapshots__/months.test.ts.snap +21 -0
  287. package/src/shared/arithmetic.test.ts +112 -0
  288. package/src/shared/arithmetic.ts +170 -0
  289. package/src/shared/async.test.ts +135 -0
  290. package/src/shared/async.ts +76 -0
  291. package/src/shared/constants.ts +5 -0
  292. package/src/shared/currencies.ts +70 -0
  293. package/src/shared/dashboard.ts +260 -0
  294. package/src/shared/environment.ts +18 -0
  295. package/src/shared/errors.ts +195 -0
  296. package/src/shared/locale.ts +27 -0
  297. package/src/shared/location-utils.test.ts +69 -0
  298. package/src/shared/location-utils.ts +49 -0
  299. package/src/shared/months.test.ts +5 -0
  300. package/src/shared/months.ts +485 -0
  301. package/src/shared/normalisation.ts +6 -0
  302. package/src/shared/platform.electron.ts +21 -0
  303. package/src/shared/platform.ts +20 -0
  304. package/src/shared/query.ts +176 -0
  305. package/src/shared/rules.test.ts +56 -0
  306. package/src/shared/rules.ts +371 -0
  307. package/src/shared/schedules.test.ts +570 -0
  308. package/src/shared/schedules.ts +560 -0
  309. package/src/shared/test-helpers.ts +156 -0
  310. package/src/shared/transactions.test.ts +275 -0
  311. package/src/shared/transactions.ts +433 -0
  312. package/src/shared/transfer.test.ts +75 -0
  313. package/src/shared/transfer.ts +16 -0
  314. package/src/shared/user.ts +4 -0
  315. package/src/shared/util.test.ts +240 -0
  316. package/src/shared/util.ts +633 -0
  317. package/src/types/api-handlers.ts +287 -0
  318. package/src/types/budget.ts +8 -0
  319. package/src/types/file.ts +47 -0
  320. package/src/types/handlers.ts +46 -0
  321. package/src/types/models/account.ts +24 -0
  322. package/src/types/models/bank-sync.ts +23 -0
  323. package/src/types/models/bank.ts +6 -0
  324. package/src/types/models/category-group.ts +11 -0
  325. package/src/types/models/category.ts +13 -0
  326. package/src/types/models/dashboard.ts +199 -0
  327. package/src/types/models/gocardless.ts +84 -0
  328. package/src/types/models/import-transaction.ts +56 -0
  329. package/src/types/models/index.ts +23 -0
  330. package/src/types/models/nearby-payee.ts +7 -0
  331. package/src/types/models/note.ts +4 -0
  332. package/src/types/models/openid.ts +8 -0
  333. package/src/types/models/payee-location.ts +8 -0
  334. package/src/types/models/payee.ts +10 -0
  335. package/src/types/models/pluggyai.ts +19 -0
  336. package/src/types/models/reports.ts +144 -0
  337. package/src/types/models/rule.ts +174 -0
  338. package/src/types/models/schedule.ts +49 -0
  339. package/src/types/models/simplefin.ts +28 -0
  340. package/src/types/models/tags.ts +6 -0
  341. package/src/types/models/templates.ts +135 -0
  342. package/src/types/models/transaction-filter.ts +9 -0
  343. package/src/types/models/transaction.ts +39 -0
  344. package/src/types/models/user-access.ts +10 -0
  345. package/src/types/models/user.ts +25 -0
  346. package/src/types/prefs.ts +167 -0
  347. package/src/types/server-events.ts +86 -0
  348. package/src/types/server-handlers.ts +27 -0
  349. package/src/types/util.ts +26 -0
  350. package/tsconfig.json +34 -0
  351. package/typings/pegjs.ts +1 -0
  352. package/typings/process-worker.ts +12 -0
  353. package/typings/vite-plugin-peggy-loader.ts +1 -0
  354. package/typings/window.ts +62 -0
  355. package/vite.config.ts +109 -0
  356. package/vite.desktop.config.ts +59 -0
  357. package/vitest.config.ts +43 -0
  358. package/vitest.web.config.ts +38 -0
@@ -0,0 +1,467 @@
1
+ // @ts-strict-ignore
2
+ import AdmZip from 'adm-zip';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+
5
+ import * as asyncStorage from '../platform/server/asyncStorage';
6
+ import { fetch } from '../platform/server/fetch';
7
+ import * as fs from '../platform/server/fs';
8
+ import { logger } from '../platform/server/log';
9
+ import * as sqlite from '../platform/server/sqlite';
10
+ import * as monthUtils from '../shared/months';
11
+
12
+ import * as encryption from './encryption';
13
+ import {
14
+ FileDownloadError,
15
+ FileUploadError,
16
+ HTTPError,
17
+ PostError,
18
+ } from './errors';
19
+ import { runMutator } from './mutators';
20
+ import { post } from './post';
21
+ import * as prefs from './prefs';
22
+ import { getServer } from './server-config';
23
+
24
+ const UPLOAD_FREQUENCY_IN_DAYS = 7;
25
+
26
+ export type UsersWithAccess = {
27
+ userId: string;
28
+ userName: string;
29
+ displayName: string;
30
+ owner: boolean;
31
+ };
32
+ export type RemoteFile = {
33
+ deleted: boolean;
34
+ fileId: string;
35
+ groupId: string;
36
+ name: string;
37
+ encryptKeyId: string;
38
+ hasKey: boolean;
39
+ owner: string;
40
+ usersWithAccess: UsersWithAccess[];
41
+ };
42
+
43
+ async function checkHTTPStatus(res) {
44
+ if (res.status !== 200) {
45
+ if (res.status === 403) {
46
+ try {
47
+ const text = await res.text();
48
+ const data = JSON.parse(text)?.data;
49
+ if (data?.reason === 'token-expired') {
50
+ await asyncStorage.removeItem('user-token');
51
+ throw new HTTPError(403, 'token-expired');
52
+ }
53
+ } catch (e) {
54
+ if (e instanceof HTTPError) throw e;
55
+ }
56
+ }
57
+ return res.text().then(str => {
58
+ throw new HTTPError(res.status, str);
59
+ });
60
+ } else {
61
+ return res;
62
+ }
63
+ }
64
+
65
+ async function fetchJSON(...args: Parameters<typeof fetch>) {
66
+ let res = await fetch(...args);
67
+ res = await checkHTTPStatus(res);
68
+ return res.json();
69
+ }
70
+
71
+ export async function checkKey(): Promise<{
72
+ valid: boolean;
73
+ error?: { reason: string };
74
+ }> {
75
+ const userToken = await asyncStorage.getItem('user-token');
76
+
77
+ const { cloudFileId, encryptKeyId } = prefs.getPrefs();
78
+
79
+ let res;
80
+ try {
81
+ res = await post(getServer().SYNC_SERVER + '/user-get-key', {
82
+ token: userToken,
83
+ fileId: cloudFileId,
84
+ });
85
+ } catch (e) {
86
+ logger.log(e);
87
+ return { valid: false, error: { reason: 'network' } };
88
+ }
89
+
90
+ return {
91
+ valid:
92
+ // This == comparison is important, they could be null or undefined
93
+ // oxlint-disable-next-line eslint/eqeqeq
94
+ res.id == encryptKeyId &&
95
+ (encryptKeyId == null || encryption.hasKey(encryptKeyId)),
96
+ };
97
+ }
98
+
99
+ export async function resetSyncState(newKeyState) {
100
+ const userToken = await asyncStorage.getItem('user-token');
101
+
102
+ const { cloudFileId } = prefs.getPrefs();
103
+
104
+ try {
105
+ await post(getServer().SYNC_SERVER + '/reset-user-file', {
106
+ token: userToken,
107
+ fileId: cloudFileId,
108
+ });
109
+ } catch (e) {
110
+ if (e instanceof PostError) {
111
+ return {
112
+ error: {
113
+ reason: e.reason === 'unauthorized' ? 'unauthorized' : 'network',
114
+ },
115
+ };
116
+ }
117
+ return { error: { reason: 'internal' } };
118
+ }
119
+
120
+ if (newKeyState) {
121
+ try {
122
+ await post(getServer().SYNC_SERVER + '/user-create-key', {
123
+ token: userToken,
124
+ fileId: cloudFileId,
125
+ keyId: newKeyState.key.getId(),
126
+ keySalt: newKeyState.salt,
127
+ testContent: newKeyState.testContent,
128
+ });
129
+ } catch (e) {
130
+ if (e instanceof PostError) {
131
+ return { error: { reason: 'network' } };
132
+ }
133
+ return { error: { reason: 'internal' } };
134
+ }
135
+ }
136
+
137
+ return {};
138
+ }
139
+
140
+ export async function exportBuffer() {
141
+ const { id, budgetName } = prefs.getPrefs();
142
+ if (!budgetName) {
143
+ return null;
144
+ }
145
+
146
+ const budgetDir = fs.getBudgetDir(id);
147
+
148
+ // create zip
149
+ const zipped = new AdmZip();
150
+
151
+ // We run this in a mutator even though its not mutating anything
152
+ // because we are reading the sqlite file from disk. We want to make
153
+ // sure that we get a valid snapshot of it so we want this to be
154
+ // serialized with all other mutations.
155
+ await runMutator(async () => {
156
+ const rawDbContent = await fs.readFile(
157
+ fs.join(budgetDir, 'db.sqlite'),
158
+ 'binary',
159
+ );
160
+
161
+ // Do some post-processing of the database. We NEVER upload the cache with
162
+ // the database; this forces new downloads to always recompute everything
163
+ // which is not only safer, but reduces the filesize a lot.
164
+ const memDb = await sqlite.openDatabase(rawDbContent);
165
+ sqlite.execQuery(
166
+ memDb,
167
+ `
168
+ DELETE FROM kvcache;
169
+ DELETE FROM kvcache_key;
170
+ `,
171
+ );
172
+
173
+ const dbContent = await sqlite.exportDatabase(memDb);
174
+
175
+ sqlite.closeDatabase(memDb);
176
+
177
+ // mark it as a file that needs a new clock so when a new client
178
+ // downloads it, it'll get set to a unique node
179
+ const meta = JSON.parse(
180
+ await fs.readFile(fs.join(budgetDir, 'metadata.json')),
181
+ );
182
+
183
+ meta.resetClock = true;
184
+ const metaContent = Buffer.from(JSON.stringify(meta), 'utf8');
185
+
186
+ zipped.addFile('db.sqlite', Buffer.from(dbContent));
187
+ zipped.addFile('metadata.json', metaContent);
188
+ });
189
+
190
+ return Buffer.from(zipped.toBuffer());
191
+ }
192
+
193
+ export async function importBuffer(fileData, buffer) {
194
+ let zipped, entries;
195
+ try {
196
+ zipped = new AdmZip(buffer);
197
+ entries = zipped.getEntries();
198
+ } catch {
199
+ throw FileDownloadError('not-zip-file');
200
+ }
201
+ const dbEntry = entries.find(e => e.entryName.includes('db.sqlite'));
202
+ const metaEntry = entries.find(e => e.entryName.includes('metadata.json'));
203
+
204
+ if (!dbEntry || !metaEntry) {
205
+ throw FileDownloadError('invalid-zip-file');
206
+ }
207
+
208
+ const dbContent = zipped.readFile(dbEntry);
209
+ const metaContent = zipped.readFile(metaEntry);
210
+
211
+ let meta;
212
+ try {
213
+ meta = JSON.parse(metaContent.toString('utf8'));
214
+ } catch {
215
+ throw FileDownloadError('invalid-meta-file');
216
+ }
217
+
218
+ // Update the metadata. The stored file on the server might be
219
+ // out-of-date with a few keys
220
+ meta = {
221
+ ...meta,
222
+ cloudFileId: fileData.fileId,
223
+ groupId: fileData.groupId,
224
+ lastUploaded: monthUtils.currentDay(),
225
+ encryptKeyId: fileData.encryptMeta ? fileData.encryptMeta.keyId : null,
226
+ };
227
+
228
+ const budgetDir = fs.getBudgetDir(meta.id);
229
+
230
+ if (await fs.exists(budgetDir)) {
231
+ // Don't remove the directory so that backups are retained
232
+ const dbFile = fs.join(budgetDir, 'db.sqlite');
233
+ const metaFile = fs.join(budgetDir, 'metadata.json');
234
+
235
+ if (await fs.exists(dbFile)) {
236
+ await fs.removeFile(dbFile);
237
+ }
238
+ if (await fs.exists(metaFile)) {
239
+ await fs.removeFile(metaFile);
240
+ }
241
+ } else {
242
+ await fs.mkdir(budgetDir);
243
+ }
244
+
245
+ await fs.writeFile(fs.join(budgetDir, 'db.sqlite'), dbContent);
246
+ await fs.writeFile(fs.join(budgetDir, 'metadata.json'), JSON.stringify(meta));
247
+
248
+ return { id: meta.id };
249
+ }
250
+
251
+ export async function upload() {
252
+ const userToken = await asyncStorage.getItem('user-token');
253
+ if (!userToken) {
254
+ throw FileUploadError('unauthorized');
255
+ }
256
+
257
+ const zipContent = await exportBuffer();
258
+ if (zipContent == null) {
259
+ return;
260
+ }
261
+
262
+ const {
263
+ id,
264
+ groupId,
265
+ budgetName,
266
+ cloudFileId: originalCloudFileId,
267
+ encryptKeyId,
268
+ } = prefs.getPrefs();
269
+ let cloudFileId = originalCloudFileId;
270
+ let uploadContent = zipContent;
271
+ let uploadMeta = null;
272
+
273
+ // The upload process encrypts with the key tagged in the prefs for
274
+ // the file. It will upload the file and the server is responsible
275
+ // for checking that the key is up-to-date and rejecting it if not
276
+ if (encryptKeyId) {
277
+ let encrypted;
278
+ try {
279
+ encrypted = await encryption.encrypt(zipContent, encryptKeyId);
280
+ } catch (e) {
281
+ throw FileUploadError('encrypt-failure', {
282
+ isMissingKey: e.message === 'missing-key',
283
+ });
284
+ }
285
+ uploadContent = encrypted.value;
286
+ uploadMeta = encrypted.meta;
287
+ }
288
+
289
+ if (!cloudFileId) {
290
+ cloudFileId = uuidv4();
291
+ }
292
+
293
+ let res;
294
+ try {
295
+ res = await fetchJSON(getServer().SYNC_SERVER + '/upload-user-file', {
296
+ method: 'POST',
297
+ headers: {
298
+ 'Content-Length': String(uploadContent.length),
299
+ 'Content-Type': 'application/encrypted-file',
300
+ 'X-ACTUAL-TOKEN': userToken,
301
+ 'X-ACTUAL-FILE-ID': cloudFileId,
302
+ 'X-ACTUAL-NAME': encodeURIComponent(budgetName),
303
+ 'X-ACTUAL-FORMAT': '2',
304
+ ...(uploadMeta
305
+ ? { 'X-ACTUAL-ENCRYPT-META': JSON.stringify(uploadMeta) }
306
+ : null),
307
+ ...(groupId ? { 'X-ACTUAL-GROUP-ID': groupId } : null),
308
+ // TODO: fix me
309
+ // oxlint-disable-next-line typescript/no-explicit-any
310
+ },
311
+ body: uploadContent,
312
+ });
313
+ } catch (err) {
314
+ logger.log('Upload failure', err);
315
+
316
+ if (err instanceof PostError) {
317
+ throw FileUploadError(
318
+ err.reason === 'unauthorized'
319
+ ? 'unauthorized'
320
+ : err.reason || 'network',
321
+ );
322
+ }
323
+
324
+ throw FileUploadError('internal');
325
+ }
326
+
327
+ if (res.status === 'ok') {
328
+ // Only save it if we are still working on the same file
329
+ if (prefs.getPrefs() && prefs.getPrefs().id === id) {
330
+ await prefs.savePrefs({
331
+ lastUploaded: monthUtils.currentDay(),
332
+ cloudFileId,
333
+ groupId: res.groupId,
334
+ });
335
+ }
336
+ } else {
337
+ throw FileUploadError('internal');
338
+ }
339
+ }
340
+
341
+ export async function possiblyUpload() {
342
+ const { cloudFileId, groupId, lastUploaded } = prefs.getPrefs();
343
+
344
+ const threshold =
345
+ lastUploaded && monthUtils.addDays(lastUploaded, UPLOAD_FREQUENCY_IN_DAYS);
346
+ const currentDay = monthUtils.currentDay();
347
+
348
+ // We only want to try to upload every UPLOAD_FREQUENCY_IN_DAYS days
349
+ if (lastUploaded && currentDay < threshold) {
350
+ return;
351
+ }
352
+
353
+ // We only want to upload existing cloud files that are part of a
354
+ // valid group
355
+ if (!cloudFileId || !groupId) {
356
+ return;
357
+ }
358
+
359
+ // Don't block on uploading
360
+ upload().catch(() => {
361
+ // Ignore errors
362
+ });
363
+ }
364
+
365
+ export async function removeFile(fileId) {
366
+ const userToken = await asyncStorage.getItem('user-token');
367
+
368
+ await post(getServer().SYNC_SERVER + '/delete-user-file', {
369
+ token: userToken,
370
+ fileId,
371
+ });
372
+ }
373
+
374
+ export async function listRemoteFiles(): Promise<RemoteFile[]> {
375
+ const userToken = await asyncStorage.getItem('user-token');
376
+ if (!userToken) {
377
+ return null;
378
+ }
379
+
380
+ let res;
381
+ try {
382
+ res = await fetchJSON(getServer().SYNC_SERVER + '/list-user-files', {
383
+ headers: {
384
+ 'X-ACTUAL-TOKEN': userToken,
385
+ },
386
+ });
387
+ } catch (e) {
388
+ logger.log('Unexpected error fetching file list from server', e);
389
+ return null;
390
+ }
391
+
392
+ if (res.status === 'error') {
393
+ logger.log('Error fetching file list from server', res);
394
+ return null;
395
+ }
396
+
397
+ return res.data
398
+ .map(file => ({
399
+ ...file,
400
+ hasKey: encryption.hasKey(file.encryptKeyId),
401
+ }))
402
+ .filter(Boolean);
403
+ }
404
+
405
+ export async function download(cloudFileId) {
406
+ const userToken = await asyncStorage.getItem('user-token');
407
+ const syncServer = getServer().SYNC_SERVER;
408
+
409
+ const userFileFetch = fetch(`${syncServer}/download-user-file`, {
410
+ headers: {
411
+ 'X-ACTUAL-TOKEN': userToken,
412
+ 'X-ACTUAL-FILE-ID': cloudFileId,
413
+ },
414
+ })
415
+ .then(checkHTTPStatus)
416
+ .then(res => {
417
+ if (res.arrayBuffer) {
418
+ return res.arrayBuffer().then(ab => Buffer.from(ab));
419
+ }
420
+ return res.buffer();
421
+ })
422
+ .catch(err => {
423
+ logger.log('Download failure', err);
424
+ throw FileDownloadError('download-failure');
425
+ });
426
+
427
+ const userFileInfoFetch = fetchJSON(`${syncServer}/get-user-file-info`, {
428
+ headers: {
429
+ 'X-ACTUAL-TOKEN': userToken,
430
+ 'X-ACTUAL-FILE-ID': cloudFileId,
431
+ },
432
+ }).catch(err => {
433
+ logger.log('Error fetching file info', err);
434
+ throw FileDownloadError('internal', { fileId: cloudFileId });
435
+ });
436
+
437
+ const [userFileInfoRes, userFileRes] = await Promise.all([
438
+ userFileInfoFetch,
439
+ userFileFetch,
440
+ ]);
441
+
442
+ if (userFileInfoRes.status !== 'ok') {
443
+ logger.log(
444
+ 'Could not download file from the server. Are you sure you have the right file ID?',
445
+ userFileInfoRes,
446
+ );
447
+ throw FileDownloadError('internal', { fileId: cloudFileId });
448
+ }
449
+
450
+ const fileData = userFileInfoRes.data;
451
+ let buffer = userFileRes;
452
+
453
+ // The download process checks if the server gave us decrypt
454
+ // information. It is assumed that this key has already been loaded
455
+ // in, which is done in a previous step
456
+ if (fileData.encryptMeta) {
457
+ try {
458
+ buffer = await encryption.decrypt(buffer, fileData.encryptMeta);
459
+ } catch (e) {
460
+ throw FileDownloadError('decrypt-failure', {
461
+ isMissingKey: e.message === 'missing-key',
462
+ });
463
+ }
464
+ }
465
+
466
+ return importBuffer(fileData, buffer);
467
+ }