@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,89 @@
1
+ // @ts-strict-ignore
2
+ import crypto from 'crypto';
3
+
4
+ import type * as T from './encryption-internals';
5
+
6
+ const ENCRYPTION_ALGORITHM = 'aes-256-gcm' as const;
7
+
8
+ export const randomBytes: typeof T.randomBytes = n => {
9
+ return crypto.randomBytes(n);
10
+ };
11
+
12
+ export const encrypt: typeof T.encrypt = async (masterKey, value) => {
13
+ const masterKeyBuffer = masterKey.getValue().raw;
14
+ // let iv = createKeyBuffer({ numBytes: 12, secret: masterKeyBuffer });
15
+ const iv = crypto.randomBytes(12);
16
+ const cipher = crypto.createCipheriv(
17
+ ENCRYPTION_ALGORITHM,
18
+ masterKeyBuffer,
19
+ iv,
20
+ );
21
+ let encrypted = cipher.update(value);
22
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
23
+
24
+ const authTag = cipher.getAuthTag();
25
+
26
+ return {
27
+ value: encrypted,
28
+ meta: {
29
+ keyId: masterKey.getId(),
30
+ algorithm: ENCRYPTION_ALGORITHM,
31
+ iv: iv.toString('base64'),
32
+ authTag: authTag.toString('base64'),
33
+ },
34
+ };
35
+ };
36
+
37
+ export const decrypt: typeof T.decrypt = async (masterKey, encrypted, meta) => {
38
+ const masterKeyBuffer = masterKey.getValue().raw;
39
+ const { algorithm, iv: originalIv, authTag: originalAuthTag } = meta;
40
+ const iv = Buffer.from(originalIv, 'base64');
41
+ const authTag = Buffer.from(originalAuthTag, 'base64');
42
+
43
+ const decipher = crypto.createDecipheriv(algorithm, masterKeyBuffer, iv);
44
+ decipher.setAuthTag(authTag);
45
+
46
+ let decrypted = decipher.update(encrypted);
47
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
48
+ return decrypted;
49
+ };
50
+
51
+ // @ts-expect-error fix me
52
+ export const createKey: typeof T.createKey = async ({ secret, salt }) => {
53
+ const buffer = createKeyBuffer({ secret, salt });
54
+ return {
55
+ raw: buffer,
56
+ base64: buffer.toString('base64'),
57
+ };
58
+ };
59
+
60
+ // @ts-expect-error fix me
61
+ export const importKey: typeof T.importKey = str => {
62
+ return {
63
+ raw: Buffer.from(str, 'base64'),
64
+ base64: str,
65
+ };
66
+ };
67
+
68
+ /**
69
+ * Generates a Buffer of a desired byte length to be used as either an encryption key or an initialization vector.
70
+ *
71
+ * @private
72
+ */
73
+ function createKeyBuffer({
74
+ numBytes,
75
+ secret,
76
+ salt,
77
+ }: {
78
+ numBytes?: number;
79
+ secret?: string;
80
+ salt?: string;
81
+ }) {
82
+ return crypto.pbkdf2Sync(
83
+ secret || crypto.randomBytes(128).toString('base64'),
84
+ salt || crypto.randomBytes(32).toString('base64'),
85
+ 10000,
86
+ numBytes || 32,
87
+ 'sha512',
88
+ );
89
+ }
@@ -0,0 +1,109 @@
1
+ // @ts-strict-ignore
2
+ const ENCRYPTION_ALGORITHM = 'aes-256-gcm';
3
+
4
+ function browserAlgorithmName(name) {
5
+ switch (name) {
6
+ case 'aes-256-gcm':
7
+ return 'AES-GCM';
8
+ default:
9
+ throw new Error('unsupported crypto algorithm: ' + name);
10
+ }
11
+ }
12
+
13
+ export function randomBytes(n) {
14
+ return Buffer.from(crypto.getRandomValues(new Uint8Array(n)));
15
+ }
16
+
17
+ export async function encrypt(masterKey, value) {
18
+ const iv = crypto.getRandomValues(new Uint8Array(12));
19
+
20
+ const encryptedArrayBuffer = await crypto.subtle.encrypt(
21
+ {
22
+ name: browserAlgorithmName(ENCRYPTION_ALGORITHM),
23
+ iv,
24
+ tagLength: 128,
25
+ },
26
+ masterKey.getValue().raw,
27
+ value,
28
+ );
29
+
30
+ const encrypted = Buffer.from(encryptedArrayBuffer);
31
+
32
+ // Strip the auth tag off the end
33
+ const authTag = encrypted.slice(-16);
34
+ const strippedEncrypted = encrypted.slice(0, -16);
35
+
36
+ return {
37
+ value: strippedEncrypted,
38
+ meta: {
39
+ keyId: masterKey.getId(),
40
+ algorithm: ENCRYPTION_ALGORITHM,
41
+ iv: Buffer.from(iv).toString('base64'),
42
+ authTag: authTag.toString('base64'),
43
+ },
44
+ };
45
+ }
46
+
47
+ export async function decrypt(masterKey, encrypted, meta) {
48
+ const { algorithm, iv, authTag } = meta;
49
+
50
+ const decrypted = await crypto.subtle.decrypt(
51
+ {
52
+ name: browserAlgorithmName(algorithm),
53
+ iv: Buffer.from(iv, 'base64'),
54
+ tagLength: 128,
55
+ },
56
+ masterKey.getValue().raw,
57
+ Buffer.concat([encrypted, Buffer.from(authTag, 'base64')]),
58
+ );
59
+
60
+ return Buffer.from(decrypted);
61
+ }
62
+
63
+ export async function createKey({ secret, salt }) {
64
+ const passwordBuffer = Buffer.from(secret);
65
+ const saltBuffer = Buffer.from(salt);
66
+
67
+ const passwordKey = await crypto.subtle.importKey(
68
+ 'raw',
69
+ passwordBuffer,
70
+ { name: 'PBKDF2' },
71
+ false,
72
+ ['deriveBits', 'deriveKey'],
73
+ );
74
+
75
+ const derivedKey = await crypto.subtle.deriveKey(
76
+ {
77
+ name: 'PBKDF2',
78
+ hash: 'SHA-512',
79
+ salt: saltBuffer,
80
+ iterations: 10000,
81
+ },
82
+ passwordKey,
83
+ { name: 'AES-GCM', length: 256 },
84
+ true,
85
+ ['encrypt', 'decrypt'],
86
+ );
87
+
88
+ const exported = await crypto.subtle.exportKey('raw', derivedKey);
89
+
90
+ return {
91
+ raw: derivedKey,
92
+ base64: Buffer.from(exported).toString('base64'),
93
+ };
94
+ }
95
+
96
+ export async function importKey(str) {
97
+ const key = await crypto.subtle.importKey(
98
+ 'raw',
99
+ Buffer.from(str, 'base64'),
100
+ { name: 'AES-GCM' },
101
+ false,
102
+ ['encrypt', 'decrypt'],
103
+ );
104
+
105
+ return {
106
+ raw: key,
107
+ base64: str,
108
+ };
109
+ }
@@ -0,0 +1,19 @@
1
+ import * as encryption from '.';
2
+
3
+ afterEach(() => encryption.unloadAllKeys());
4
+
5
+ describe('Encryption', () => {
6
+ test('should encrypt and decrypt', async () => {
7
+ const key = await encryption.createKey({
8
+ id: 'foo',
9
+ password: 'mypassword',
10
+ salt: 'salt',
11
+ });
12
+ await encryption.loadKey(key);
13
+
14
+ const data = await encryption.encrypt('hello', 'foo');
15
+
16
+ const output = await encryption.decrypt(data.value, data.meta);
17
+ expect(output.toString()).toBe('hello');
18
+ });
19
+ });
@@ -0,0 +1,19 @@
1
+ import * as encryption from '.';
2
+
3
+ afterEach(() => encryption.unloadAllKeys());
4
+
5
+ describe('Encryption', () => {
6
+ test('should encrypt and decrypt', async () => {
7
+ const key = await encryption.createKey({
8
+ id: 'foo',
9
+ password: 'mypassword',
10
+ salt: 'salt',
11
+ });
12
+ await encryption.loadKey(key);
13
+
14
+ const data = await encryption.encrypt('hello', 'foo');
15
+
16
+ const output = await encryption.decrypt(data.value, data.meta);
17
+ expect(output.toString()).toBe('hello');
18
+ });
19
+ });
@@ -0,0 +1,89 @@
1
+ // @ts-strict-ignore
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ import * as internals from './encryption-internals';
5
+
6
+ // A map of all possible master encryption keys to use, keyed by
7
+ // unique id
8
+ let keys = {};
9
+
10
+ class Key {
11
+ id;
12
+ value;
13
+
14
+ constructor({ id }) {
15
+ this.id = id || uuidv4();
16
+ }
17
+
18
+ async createFromPassword({ password, salt }) {
19
+ this.value = await internals.createKey({ secret: password, salt });
20
+ }
21
+
22
+ async createFromBase64(str) {
23
+ this.value = await internals.importKey(str);
24
+ }
25
+
26
+ getId() {
27
+ return this.id;
28
+ }
29
+
30
+ getValue() {
31
+ return this.value;
32
+ }
33
+
34
+ serialize() {
35
+ return {
36
+ id: this.id,
37
+ base64: this.value.base64,
38
+ };
39
+ }
40
+ }
41
+
42
+ export function getKey(keyId) {
43
+ if (keyId == null || keys[keyId] == null) {
44
+ throw new Error('missing-key');
45
+ }
46
+ return keys[keyId];
47
+ }
48
+
49
+ export function hasKey(keyId) {
50
+ return keyId in keys;
51
+ }
52
+
53
+ export async function encrypt(value, keyId) {
54
+ return internals.encrypt(getKey(keyId), value);
55
+ }
56
+
57
+ export async function decrypt(encrypted, meta) {
58
+ return internals.decrypt(getKey(meta.keyId), encrypted, meta);
59
+ }
60
+
61
+ export function randomBytes(n) {
62
+ return internals.randomBytes(n);
63
+ }
64
+
65
+ export async function loadKey(key) {
66
+ let keyInstance;
67
+ if (!(key instanceof Key)) {
68
+ keyInstance = new Key({ id: key.id });
69
+ await keyInstance.createFromBase64(key.base64);
70
+ } else {
71
+ keyInstance = key;
72
+ }
73
+
74
+ keys[keyInstance.getId()] = keyInstance;
75
+ }
76
+
77
+ export function unloadKey(key) {
78
+ delete keys[key.getId()];
79
+ }
80
+
81
+ export function unloadAllKeys() {
82
+ keys = {};
83
+ }
84
+
85
+ export async function createKey({ id, password, salt }) {
86
+ const key = new Key({ id });
87
+ await key.createFromPassword({ password, salt });
88
+ return key;
89
+ }
@@ -0,0 +1,110 @@
1
+ // TODO: normalize error types
2
+ export class PostError extends Error {
3
+ meta: { meta: string } | undefined;
4
+ reason: string;
5
+ type: 'PostError';
6
+
7
+ constructor(reason: string, meta?: { meta: string }) {
8
+ super('PostError: ' + reason);
9
+ this.type = 'PostError';
10
+ this.reason = reason;
11
+ this.meta = meta;
12
+ }
13
+ }
14
+
15
+ export class BankSyncError extends Error {
16
+ reason: string;
17
+ category: string;
18
+ code: string;
19
+ type: 'BankSyncError';
20
+
21
+ constructor(reason: string, category: string, code: string) {
22
+ super('BankSyncError: ' + reason);
23
+ this.type = 'BankSyncError';
24
+ this.reason = reason;
25
+ this.category = category;
26
+ this.code = code;
27
+ }
28
+ }
29
+
30
+ export class HTTPError extends Error {
31
+ statusCode: number;
32
+ responseBody: string;
33
+
34
+ constructor(code: number, body: string) {
35
+ super(`HTTPError: unsuccessful status code (${code}): ${body}`);
36
+ this.statusCode = code;
37
+ this.responseBody = body;
38
+ }
39
+ }
40
+
41
+ export class SyncError extends Error {
42
+ meta:
43
+ | {
44
+ isMissingKey: boolean;
45
+ }
46
+ | {
47
+ error: { message: string; stack: string };
48
+ query: { sql: string; params: Array<string | number> };
49
+ }
50
+ | undefined;
51
+ reason: string;
52
+
53
+ constructor(
54
+ reason: string,
55
+ meta?:
56
+ | {
57
+ isMissingKey: boolean;
58
+ }
59
+ | {
60
+ error: { message: string; stack: string };
61
+ query: { sql: string; params: Array<string | number> };
62
+ },
63
+ ) {
64
+ super('SyncError: ' + reason);
65
+ this.reason = reason;
66
+ this.meta = meta;
67
+ }
68
+ }
69
+
70
+ export class ValidationError extends Error {}
71
+
72
+ export class TransactionError extends Error {}
73
+
74
+ export class RuleError extends Error {
75
+ type: string;
76
+
77
+ constructor(name: string, message: string) {
78
+ super('RuleError: ' + message);
79
+ this.type = name;
80
+ }
81
+ }
82
+
83
+ export function APIError(
84
+ msg: string,
85
+ meta?: {
86
+ conditionErrors: string[];
87
+ actionErrors: string[];
88
+ },
89
+ ) {
90
+ return { type: 'APIError', message: msg, meta };
91
+ }
92
+
93
+ export function FileDownloadError(
94
+ reason: string,
95
+ meta?: {
96
+ fileId?: string;
97
+ isMissingKey?: boolean;
98
+ name?: string;
99
+ id?: string;
100
+ },
101
+ ) {
102
+ return { type: 'FileDownloadError', reason, meta };
103
+ }
104
+
105
+ export function FileUploadError(
106
+ reason: string,
107
+ meta?: { isMissingKey: boolean },
108
+ ) {
109
+ return { type: 'FileUploadError', reason, meta };
110
+ }
@@ -0,0 +1,191 @@
1
+ // @ts-strict-ignore
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ import type { TransactionFilterEntity } from '../../types/models';
5
+ import { createApp } from '../app';
6
+ import * as db from '../db';
7
+ import { requiredFields } from '../models';
8
+ import { mutator } from '../mutators';
9
+ import { parseConditionsOrActions } from '../transactions/transaction-rules';
10
+ import { undoable } from '../undo';
11
+
12
+ const filterModel = {
13
+ validate(filter, { update }: { update?: boolean } = {}) {
14
+ requiredFields('transaction_filters', filter, ['conditions'], update);
15
+
16
+ if (!update || 'conditionsOp' in filter) {
17
+ if (!['and', 'or'].includes(filter.conditionsOp)) {
18
+ throw new Error('Invalid filter conditionsOp: ' + filter.conditionsOp);
19
+ }
20
+ }
21
+
22
+ return filter;
23
+ },
24
+
25
+ toJS(row) {
26
+ const { conditions, conditions_op, ...fields } = row;
27
+ return {
28
+ ...fields,
29
+ conditionsOp: conditions_op,
30
+ conditions: parseConditionsOrActions(conditions),
31
+ };
32
+ },
33
+
34
+ fromJS(filter) {
35
+ const { conditionsOp, ...row } = filter;
36
+ if (conditionsOp) {
37
+ row.conditions_op = conditionsOp;
38
+ }
39
+ return row;
40
+ },
41
+ };
42
+
43
+ async function filterNameExists(name, filterId, newItem) {
44
+ const idForName = await db.first<Pick<db.DbTransactionFilter, 'id'>>(
45
+ 'SELECT id from transaction_filters WHERE tombstone = 0 AND name = ?',
46
+ [name],
47
+ );
48
+
49
+ if (idForName === null) {
50
+ return false;
51
+ }
52
+ if (!newItem) {
53
+ return idForName.id !== filterId;
54
+ }
55
+ return true;
56
+ }
57
+
58
+ function conditionExists(item, filters, newItem) {
59
+ const { conditions, conditionsOp } = item;
60
+ let fConditionFound = null;
61
+
62
+ filters.some(filter => {
63
+ if (
64
+ (conditions.length === 1 || filter.conditionsOp === conditionsOp) &&
65
+ !filter.tombstone &&
66
+ filter.conditions.length === conditions.length
67
+ ) {
68
+ const allConditionsMatch = !conditions.some(
69
+ cond =>
70
+ !filter.conditions.some(
71
+ fcond =>
72
+ cond.value === fcond.value &&
73
+ cond.op === fcond.op &&
74
+ cond.field === fcond.field &&
75
+ filterOptionsMatch(cond.options, fcond.options),
76
+ ),
77
+ );
78
+
79
+ if (allConditionsMatch) {
80
+ fConditionFound = filter;
81
+ return true;
82
+ }
83
+ }
84
+ return false;
85
+ });
86
+
87
+ if (!newItem) {
88
+ return fConditionFound
89
+ ? fConditionFound.id !== item.id
90
+ ? fConditionFound.name
91
+ : false
92
+ : false;
93
+ }
94
+
95
+ return fConditionFound ? fConditionFound.name : false;
96
+ }
97
+
98
+ function filterOptionsMatch(options1, options2) {
99
+ const opt1 = options1 ?? {};
100
+ const opt2 = options2 ?? {};
101
+
102
+ const keys1 = Object.keys(opt1);
103
+ const keys2 = Object.keys(opt2);
104
+
105
+ if (keys1.length !== keys2.length) {
106
+ return false;
107
+ }
108
+
109
+ return keys1.every(key => opt1[key] === opt2[key]);
110
+ }
111
+
112
+ async function createFilter(filter): Promise<TransactionFilterEntity['id']> {
113
+ const filterId = uuidv4();
114
+ const item = {
115
+ id: filterId,
116
+ conditions: filter.state.conditions,
117
+ conditionsOp: filter.state.conditionsOp,
118
+ name: filter.state.name,
119
+ };
120
+
121
+ if (item.name) {
122
+ if (await filterNameExists(item.name, item.id, true)) {
123
+ throw new Error('There is already a filter named ' + item.name);
124
+ }
125
+ } else {
126
+ throw new Error('Filter name is required');
127
+ }
128
+
129
+ if (item.conditions.length > 0) {
130
+ const condExists = conditionExists(item, filter.filters, true);
131
+ if (condExists) {
132
+ throw new Error(
133
+ 'Duplicate filter warning: conditions already exist. Filter name: ' +
134
+ condExists,
135
+ );
136
+ }
137
+ } else {
138
+ throw new Error('Conditions are required');
139
+ }
140
+
141
+ // Create the filter here based on the info
142
+ await db.insertWithSchema('transaction_filters', filterModel.fromJS(item));
143
+
144
+ return filterId;
145
+ }
146
+
147
+ async function updateFilter(filter) {
148
+ const item = {
149
+ id: filter.state.id,
150
+ conditions: filter.state.conditions,
151
+ conditionsOp: filter.state.conditionsOp,
152
+ name: filter.state.name,
153
+ };
154
+ if (item.name) {
155
+ if (await filterNameExists(item.name, item.id, false)) {
156
+ throw new Error('There is already a filter named ' + item.name);
157
+ }
158
+ } else {
159
+ throw new Error('Filter name is required');
160
+ }
161
+
162
+ if (item.conditions.length > 0) {
163
+ const condExists = conditionExists(item, filter.filters, false);
164
+ if (condExists) {
165
+ throw new Error(
166
+ 'Duplicate filter warning: conditions already exist. Filter name: ' +
167
+ condExists,
168
+ );
169
+ }
170
+ } else {
171
+ throw new Error('Conditions are required');
172
+ }
173
+
174
+ await db.updateWithSchema('transaction_filters', filterModel.fromJS(item));
175
+ }
176
+
177
+ async function deleteFilter(id: TransactionFilterEntity['id']) {
178
+ await db.delete_('transaction_filters', id);
179
+ }
180
+
181
+ export type FiltersHandlers = {
182
+ 'filter-create': typeof createFilter;
183
+ 'filter-update': typeof updateFilter;
184
+ 'filter-delete': typeof deleteFilter;
185
+ };
186
+
187
+ export const app = createApp<FiltersHandlers>();
188
+
189
+ app.method('filter-create', mutator(createFilter));
190
+ app.method('filter-update', mutator(updateFilter));
191
+ app.method('filter-delete', mutator(undoable(deleteFilter)));
@@ -0,0 +1,49 @@
1
+ // @ts-strict-ignore
2
+ import * as fs from '../../platform/server/fs';
3
+ import * as sqlite from '../../platform/server/sqlite';
4
+ import * as cloudStorage from '../cloud-storage';
5
+ import { handlers } from '../main';
6
+ import { waitOnSpreadsheet } from '../sheet';
7
+
8
+ export async function importActual(_filepath: string, buffer: Buffer) {
9
+ // Importing Actual files is a special case because we can directly
10
+ // write down the files, but because it doesn't go through the API
11
+ // layer we need to duplicate some of the workflow
12
+ await handlers['close-budget']();
13
+
14
+ let id;
15
+ try {
16
+ ({ id } = await cloudStorage.importBuffer(
17
+ { cloudFileId: null, groupId: null },
18
+ buffer,
19
+ ));
20
+ } catch (e) {
21
+ if (e.type === 'FileDownloadError') {
22
+ return { error: e.reason };
23
+ }
24
+ throw e;
25
+ }
26
+
27
+ // We never want to load cached data from imported files, so
28
+ // delete the cache
29
+ const sqliteDb = await sqlite.openDatabase(
30
+ fs.join(fs.getBudgetDir(id), 'db.sqlite'),
31
+ );
32
+ sqlite.execQuery(
33
+ sqliteDb,
34
+ `
35
+ DELETE FROM kvcache;
36
+ DELETE FROM kvcache_key;
37
+ `,
38
+ );
39
+ sqlite.closeDatabase(sqliteDb);
40
+
41
+ // Load the budget, force everything to be computed, and try
42
+ // to upload it as a cloud file
43
+ await handlers['load-budget']({ id });
44
+ await handlers['get-budget-bounds']();
45
+ await waitOnSpreadsheet();
46
+ await cloudStorage.upload().catch(() => {
47
+ // Ignore errors
48
+ });
49
+ }