@actual-app/sync-server 25.4.0-alpha.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 (137) hide show
  1. package/.dockerignore +12 -0
  2. package/README.md +19 -0
  3. package/app.js +11 -0
  4. package/babel.config.json +3 -0
  5. package/bin/@actual-app/sync-server +55 -0
  6. package/docker/alpine.Dockerfile +62 -0
  7. package/docker/ubuntu.Dockerfile +63 -0
  8. package/docker-compose.yml +29 -0
  9. package/jest.config.json +19 -0
  10. package/jest.global-setup.js +101 -0
  11. package/jest.global-teardown.js +6 -0
  12. package/migrations/1694360000000-create-folders.js +25 -0
  13. package/migrations/1694360479680-create-account-db.js +30 -0
  14. package/migrations/1694362247011-create-secret-table.js +16 -0
  15. package/migrations/1702667624000-rename-nordigen-secrets.js +19 -0
  16. package/migrations/1718889148000-openid.js +41 -0
  17. package/migrations/1719409568000-multiuser.js +116 -0
  18. package/package.json +64 -0
  19. package/src/account-db.js +239 -0
  20. package/src/accounts/openid.js +361 -0
  21. package/src/accounts/password.js +149 -0
  22. package/src/app-account.js +155 -0
  23. package/src/app-admin.js +410 -0
  24. package/src/app-admin.test.js +381 -0
  25. package/src/app-gocardless/README.md +198 -0
  26. package/src/app-gocardless/app-gocardless.js +274 -0
  27. package/src/app-gocardless/bank-factory.js +91 -0
  28. package/src/app-gocardless/banks/abanca_caglesmm.js +22 -0
  29. package/src/app-gocardless/banks/abnamro_abnanl2a.js +57 -0
  30. package/src/app-gocardless/banks/american_express_aesudef1.js +40 -0
  31. package/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +31 -0
  32. package/src/app-gocardless/banks/bank.interface.ts +51 -0
  33. package/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +39 -0
  34. package/src/app-gocardless/banks/bankinter_bkbkesmm.js +24 -0
  35. package/src/app-gocardless/banks/belfius_gkccbebb.js +17 -0
  36. package/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +61 -0
  37. package/src/app-gocardless/banks/bnp_be_gebabebb.js +73 -0
  38. package/src/app-gocardless/banks/cbc_cregbebb.js +34 -0
  39. package/src/app-gocardless/banks/commerzbank_cobadeff.js +51 -0
  40. package/src/app-gocardless/banks/danskebank_dabno22.js +39 -0
  41. package/src/app-gocardless/banks/direkt_heladef1822.js +18 -0
  42. package/src/app-gocardless/banks/easybank_bawaatww.js +50 -0
  43. package/src/app-gocardless/banks/entercard_swednokk.js +40 -0
  44. package/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +46 -0
  45. package/src/app-gocardless/banks/hype_hyeeit22.js +74 -0
  46. package/src/app-gocardless/banks/ing_ingbrobu.js +70 -0
  47. package/src/app-gocardless/banks/ing_ingddeff.js +47 -0
  48. package/src/app-gocardless/banks/ing_pl_ingbplpw.js +46 -0
  49. package/src/app-gocardless/banks/integration-bank.js +115 -0
  50. package/src/app-gocardless/banks/isybank_itbbitmm.js +18 -0
  51. package/src/app-gocardless/banks/kbc_kredbebb.js +33 -0
  52. package/src/app-gocardless/banks/lhv-lhvbee22.js +36 -0
  53. package/src/app-gocardless/banks/mbank_retail_brexplpw.js +56 -0
  54. package/src/app-gocardless/banks/nationwide_naiagb21.js +46 -0
  55. package/src/app-gocardless/banks/nbg_ethngraaxxx.js +51 -0
  56. package/src/app-gocardless/banks/norwegian_xx_norwnok1.js +74 -0
  57. package/src/app-gocardless/banks/revolut_revolt21.js +37 -0
  58. package/src/app-gocardless/banks/sandboxfinance_sfin0000.js +28 -0
  59. package/src/app-gocardless/banks/seb_kort_bank_ab.js +58 -0
  60. package/src/app-gocardless/banks/seb_privat.js +29 -0
  61. package/src/app-gocardless/banks/sparnord_spnodk22.js +24 -0
  62. package/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +61 -0
  63. package/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +30 -0
  64. package/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +19 -0
  65. package/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +50 -0
  66. package/src/app-gocardless/banks/swedbank_habalv22.js +47 -0
  67. package/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +21 -0
  68. package/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +61 -0
  69. package/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +53 -0
  70. package/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +22 -0
  71. package/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +34 -0
  72. package/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +110 -0
  73. package/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js +54 -0
  74. package/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +206 -0
  75. package/src/app-gocardless/banks/tests/ing_ingddeff.spec.js +302 -0
  76. package/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js +202 -0
  77. package/src/app-gocardless/banks/tests/integration_bank.spec.js +158 -0
  78. package/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +38 -0
  79. package/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +68 -0
  80. package/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js +171 -0
  81. package/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js +105 -0
  82. package/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js +48 -0
  83. package/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +42 -0
  84. package/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js +133 -0
  85. package/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +256 -0
  86. package/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +102 -0
  87. package/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +57 -0
  88. package/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +54 -0
  89. package/src/app-gocardless/banks/util/extract-payeeName-from-remittanceInfo.js +36 -0
  90. package/src/app-gocardless/banks/virgin_nrnbgb22.js +39 -0
  91. package/src/app-gocardless/errors.js +84 -0
  92. package/src/app-gocardless/gocardless-node.types.ts +497 -0
  93. package/src/app-gocardless/gocardless.types.ts +93 -0
  94. package/src/app-gocardless/link.html +18 -0
  95. package/src/app-gocardless/services/gocardless-service.js +620 -0
  96. package/src/app-gocardless/services/tests/fixtures.js +181 -0
  97. package/src/app-gocardless/services/tests/gocardless-service.spec.js +537 -0
  98. package/src/app-gocardless/tests/bank-factory.spec.js +20 -0
  99. package/src/app-gocardless/tests/utils.spec.js +162 -0
  100. package/src/app-gocardless/util/handle-error.js +16 -0
  101. package/src/app-gocardless/utils.js +45 -0
  102. package/src/app-openid.js +108 -0
  103. package/src/app-pluggyai/app-pluggyai.js +215 -0
  104. package/src/app-pluggyai/pluggyai-service.js +120 -0
  105. package/src/app-secrets.js +61 -0
  106. package/src/app-simplefin/app-simplefin.js +418 -0
  107. package/src/app-sync/errors.js +13 -0
  108. package/src/app-sync/services/files-service.js +243 -0
  109. package/src/app-sync/tests/services/files-service.test.js +250 -0
  110. package/src/app-sync/validation.js +77 -0
  111. package/src/app-sync.js +391 -0
  112. package/src/app-sync.test.js +877 -0
  113. package/src/app.js +145 -0
  114. package/src/config-types.ts +44 -0
  115. package/src/db.js +58 -0
  116. package/src/load-config.js +307 -0
  117. package/src/migrations.js +36 -0
  118. package/src/run-migrations.js +8 -0
  119. package/src/scripts/disable-openid.js +44 -0
  120. package/src/scripts/enable-openid.js +53 -0
  121. package/src/scripts/health-check.js +23 -0
  122. package/src/scripts/reset-password.js +51 -0
  123. package/src/secrets.test.js +83 -0
  124. package/src/services/secrets-service.js +94 -0
  125. package/src/services/user-service.js +272 -0
  126. package/src/sql/messages.sql +9 -0
  127. package/src/sync-simple.js +95 -0
  128. package/src/util/hash.js +5 -0
  129. package/src/util/middlewares.js +62 -0
  130. package/src/util/paths.js +13 -0
  131. package/src/util/payee-name.js +45 -0
  132. package/src/util/prompt.js +88 -0
  133. package/src/util/title/index.js +59 -0
  134. package/src/util/title/lower-case.js +93 -0
  135. package/src/util/title/specials.js +21 -0
  136. package/src/util/validate-user.js +68 -0
  137. package/tsconfig.json +21 -0
@@ -0,0 +1,243 @@
1
+ import { getAccountDb, isAdmin } from '../../account-db.js';
2
+ import { FileNotFound, GenericFileError } from '../errors.js';
3
+
4
+ class FileBase {
5
+ constructor(
6
+ name,
7
+ groupId,
8
+ encryptSalt,
9
+ encryptTest,
10
+ encryptKeyId,
11
+ encryptMeta,
12
+ syncVersion,
13
+ deleted,
14
+ owner,
15
+ ) {
16
+ this.name = name;
17
+ this.groupId = groupId;
18
+ this.encryptSalt = encryptSalt;
19
+ this.encryptTest = encryptTest;
20
+ this.encryptKeyId = encryptKeyId;
21
+ this.encryptMeta = encryptMeta;
22
+ this.syncVersion = syncVersion;
23
+ this.deleted = typeof deleted === 'boolean' ? deleted : Boolean(deleted);
24
+ this.owner = owner;
25
+ }
26
+ }
27
+
28
+ class File extends FileBase {
29
+ constructor({
30
+ id,
31
+ name = null,
32
+ groupId = null,
33
+ encryptSalt = null,
34
+ encryptTest = null,
35
+ encryptKeyId = null,
36
+ encryptMeta = null,
37
+ syncVersion = null,
38
+ deleted = false,
39
+ owner = null,
40
+ }) {
41
+ super(
42
+ name,
43
+ groupId,
44
+ encryptSalt,
45
+ encryptTest,
46
+ encryptKeyId,
47
+ encryptMeta,
48
+ syncVersion,
49
+ deleted,
50
+ owner,
51
+ );
52
+ this.id = id;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Represents a file update. Will only update the fields that are defined.
58
+ * @class
59
+ * @extends FileBase
60
+ */
61
+ class FileUpdate extends FileBase {
62
+ constructor({
63
+ name = undefined,
64
+ groupId = undefined,
65
+ encryptSalt = undefined,
66
+ encryptTest = undefined,
67
+ encryptKeyId = undefined,
68
+ encryptMeta = undefined,
69
+ syncVersion = undefined,
70
+ deleted = undefined,
71
+ owner = undefined,
72
+ }) {
73
+ super(
74
+ name,
75
+ groupId,
76
+ encryptSalt,
77
+ encryptTest,
78
+ encryptKeyId,
79
+ encryptMeta,
80
+ syncVersion,
81
+ deleted,
82
+ owner,
83
+ );
84
+ }
85
+ }
86
+
87
+ const boolToInt = bool => {
88
+ return bool ? 1 : 0;
89
+ };
90
+
91
+ class FilesService {
92
+ constructor(accountDb) {
93
+ this.accountDb = accountDb;
94
+ }
95
+
96
+ get(fileId) {
97
+ const rawFile = this.getRaw(fileId);
98
+ if (!rawFile || (rawFile && rawFile.deleted)) {
99
+ throw new FileNotFound();
100
+ }
101
+
102
+ return this.validate(rawFile);
103
+ }
104
+
105
+ set(file) {
106
+ const deletedInt = boolToInt(file.deleted);
107
+ this.accountDb.mutate(
108
+ 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_salt, encrypt_test, encrypt_keyid, deleted, owner) VALUES (?, ?, ?, ?, ?, ?, ?, ? ,?, ?)',
109
+ [
110
+ file.id,
111
+ file.groupId,
112
+ file.syncVersion.toString(),
113
+ file.name,
114
+ file.encryptMeta,
115
+ file.encryptSalt,
116
+ file.encrypt_test,
117
+ file.encrypt_keyid,
118
+ deletedInt,
119
+ file.owner,
120
+ ],
121
+ );
122
+ }
123
+
124
+ find({ userId, limit = 1000 }) {
125
+ const canSeeAll = isAdmin(userId);
126
+
127
+ return (
128
+ canSeeAll
129
+ ? this.accountDb.all('SELECT * FROM files WHERE deleted = 0 LIMIT ?', [
130
+ limit,
131
+ ])
132
+ : this.accountDb.all(
133
+ `SELECT files.*
134
+ FROM files
135
+ WHERE files.owner = ? and deleted = 0
136
+ UNION
137
+ SELECT files.*
138
+ FROM files
139
+ JOIN user_access
140
+ ON user_access.file_id = files.id
141
+ AND user_access.user_id = ?
142
+ WHERE files.deleted = 0 LIMIT ?`,
143
+ [userId, userId, limit],
144
+ )
145
+ ).map(this.validate);
146
+ }
147
+
148
+ findUsersWithAccess(fileId) {
149
+ const userAccess =
150
+ this.accountDb.all(
151
+ `SELECT UA.user_id as userId, users.display_name displayName, users.user_name userName
152
+ FROM files
153
+ JOIN user_access UA ON UA.file_id = files.id
154
+ JOIN users on users.id = UA.user_id
155
+ WHERE files.id = ?
156
+ UNION ALL
157
+ SELECT users.id, users.display_name, users.user_name
158
+ FROM files
159
+ JOIN users on users.id = files.owner
160
+ WHERE files.id = ?
161
+ `,
162
+ [fileId, fileId],
163
+ ) || [];
164
+
165
+ return userAccess;
166
+ }
167
+
168
+ update(id, fileUpdate) {
169
+ let query = 'UPDATE files SET';
170
+ const params = [];
171
+ const updates = [];
172
+
173
+ if (fileUpdate.name !== undefined) {
174
+ updates.push('name = ?');
175
+ params.push(fileUpdate.name);
176
+ }
177
+ if (fileUpdate.groupId !== undefined) {
178
+ updates.push('group_id = ?');
179
+ params.push(fileUpdate.groupId);
180
+ }
181
+ if (fileUpdate.encryptSalt !== undefined) {
182
+ updates.push('encrypt_salt = ?');
183
+ params.push(fileUpdate.encryptSalt);
184
+ }
185
+ if (fileUpdate.encryptTest !== undefined) {
186
+ updates.push('encrypt_test = ?');
187
+ params.push(fileUpdate.encryptTest);
188
+ }
189
+ if (fileUpdate.encryptKeyId !== undefined) {
190
+ updates.push('encrypt_keyid = ?');
191
+ params.push(fileUpdate.encryptKeyId);
192
+ }
193
+ if (fileUpdate.encryptMeta !== undefined) {
194
+ updates.push('encrypt_meta = ?');
195
+ params.push(fileUpdate.encryptMeta);
196
+ }
197
+ if (fileUpdate.syncVersion !== undefined) {
198
+ updates.push('sync_version = ?');
199
+ params.push(fileUpdate.syncVersion);
200
+ }
201
+ if (fileUpdate.deleted !== undefined) {
202
+ updates.push('deleted = ?');
203
+ params.push(boolToInt(fileUpdate.deleted));
204
+ }
205
+
206
+ if (updates.length > 0) {
207
+ query += ' ' + updates.join(', ') + ' WHERE id = ?';
208
+ params.push(id);
209
+
210
+ const res = this.accountDb.mutate(query, params);
211
+
212
+ if (res.changes !== 1) {
213
+ throw new GenericFileError('Could not update File', { id });
214
+ }
215
+ }
216
+
217
+ // Return the modified object
218
+ return this.validate(this.getRaw(id));
219
+ }
220
+
221
+ getRaw(fileId) {
222
+ return this.accountDb.first(`SELECT * FROM files WHERE id = ?`, [fileId]);
223
+ }
224
+
225
+ validate(rawFile) {
226
+ return new File({
227
+ id: rawFile.id,
228
+ name: rawFile.name,
229
+ groupId: rawFile.group_id,
230
+ encryptSalt: rawFile.encrypt_salt,
231
+ encryptTest: rawFile.encrypt_test,
232
+ encryptKeyId: rawFile.encrypt_keyid,
233
+ encryptMeta: rawFile.encrypt_meta,
234
+ syncVersion: rawFile.sync_version,
235
+ deleted: Boolean(rawFile.deleted),
236
+ owner: rawFile.owner,
237
+ });
238
+ }
239
+ }
240
+
241
+ const filesService = new FilesService(getAccountDb());
242
+
243
+ export { filesService, FilesService, File, FileUpdate };
@@ -0,0 +1,250 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ import { getAccountDb } from '../../../account-db.js';
4
+ import { FileNotFound } from '../../errors.js';
5
+ import {
6
+ FilesService,
7
+ File,
8
+ FileUpdate,
9
+ } from '../../services/files-service.js'; // Adjust the path as necessary
10
+ describe('FilesService', () => {
11
+ let filesService;
12
+ let accountDb;
13
+
14
+ const insertToyExampleData = () => {
15
+ accountDb.mutate(
16
+ 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_salt, encrypt_test, encrypt_keyid, deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
17
+ [
18
+ '1',
19
+ 'group1',
20
+ 1,
21
+ 'file1',
22
+ '{"key":"value"}',
23
+ 'salt',
24
+ 'test',
25
+ 'keyid',
26
+ 0,
27
+ ],
28
+ );
29
+ };
30
+
31
+ const clearDatabase = () => {
32
+ accountDb.mutate('DELETE FROM user_access');
33
+ accountDb.mutate('DELETE FROM files');
34
+ };
35
+
36
+ beforeAll(done => {
37
+ accountDb = getAccountDb();
38
+ filesService = new FilesService(accountDb);
39
+ done();
40
+ });
41
+
42
+ beforeEach(done => {
43
+ insertToyExampleData();
44
+ done();
45
+ });
46
+
47
+ afterEach(done => {
48
+ clearDatabase();
49
+ done();
50
+ });
51
+
52
+ test('get should return a file', () => {
53
+ const file = filesService.get('1');
54
+ const expectedFile = new File({
55
+ id: '1',
56
+ groupId: 'group1',
57
+ syncVersion: 1,
58
+ name: 'file1',
59
+ encryptMeta: '{"key":"value"}',
60
+ encryptSalt: 'salt',
61
+ encryptTest: 'test',
62
+ encryptKeyId: 'keyid',
63
+ deleted: false,
64
+ });
65
+
66
+ expect(file).toEqual(expectedFile);
67
+ });
68
+
69
+ test('get should throw FileNotFound if file is deleted or does not exist', () => {
70
+ const fileId = crypto.randomBytes(16).toString('hex');
71
+ accountDb.mutate(
72
+ 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_salt, encrypt_test, encrypt_keyid, deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
73
+ [
74
+ fileId,
75
+ 'group1',
76
+ 1,
77
+ 'file1',
78
+ '{"key":"value"}',
79
+ 'salt',
80
+ 'test',
81
+ 'keyid',
82
+ 1,
83
+ ],
84
+ );
85
+
86
+ expect(() => {
87
+ filesService.get(fileId);
88
+ }).toThrow(FileNotFound);
89
+
90
+ expect(() => {
91
+ filesService.get(crypto.randomBytes(16).toString('hex'));
92
+ }).toThrow(FileNotFound);
93
+ });
94
+
95
+ test.each([true, false])(
96
+ 'set should insert a new file with deleted: %p',
97
+ deleted => {
98
+ const fileId = crypto.randomBytes(16).toString('hex');
99
+ const newFile = new File({
100
+ id: fileId,
101
+ groupId: 'group2',
102
+ syncVersion: 1,
103
+ name: 'file2',
104
+ encryptMeta: '{"key":"value2"}',
105
+ deleted,
106
+ });
107
+
108
+ filesService.set(newFile);
109
+
110
+ const file = filesService.validate(filesService.getRaw(fileId));
111
+ const expectedFile = new File({
112
+ id: fileId,
113
+ groupId: 'group2',
114
+ syncVersion: 1,
115
+ name: 'file2',
116
+ encryptMeta: '{"key":"value2"}',
117
+ encryptSalt: null, // default value
118
+ encryptTest: null, // default value
119
+ encryptKeyId: null, // default value
120
+ deleted,
121
+ });
122
+
123
+ expect(file).toEqual(expectedFile);
124
+ },
125
+ );
126
+
127
+ test('find should return a list of files', () => {
128
+ const files = filesService.find({ userId: 'genericAdmin' });
129
+ expect(files.length).toBe(1);
130
+ expect(files[0]).toEqual(
131
+ new File({
132
+ id: '1',
133
+ groupId: 'group1',
134
+ syncVersion: 1,
135
+ name: 'file1',
136
+ encryptMeta: '{"key":"value"}',
137
+ encryptSalt: 'salt',
138
+ encryptTest: 'test',
139
+ encryptKeyId: 'keyid',
140
+ deleted: false,
141
+ }),
142
+ );
143
+ });
144
+
145
+ test('find should respect the limit parameter', () => {
146
+ filesService.set(
147
+ new File({
148
+ id: crypto.randomBytes(16).toString('hex'),
149
+ groupId: 'group2',
150
+ syncVersion: 1,
151
+ name: 'file2',
152
+ encryptMeta: '{"key":"value2"}',
153
+ deleted: false,
154
+ }),
155
+ );
156
+ // Make sure that the file was inserted
157
+ const allFiles = filesService.find({ userId: 'genericAdmin' });
158
+ expect(allFiles.length).toBe(2);
159
+
160
+ // Limit the number of files returned
161
+ const limitedFiles = filesService.find({
162
+ userId: 'genericAdmin',
163
+ limit: 1,
164
+ });
165
+ expect(limitedFiles.length).toBe(1);
166
+ });
167
+
168
+ test('update should modify all attributes of an existing file', () => {
169
+ const fileUpdate = new FileUpdate({
170
+ name: 'updatedFile1',
171
+ groupId: 'updatedGroup1',
172
+ encryptSalt: 'updatedSalt',
173
+ encryptTest: 'updatedTest',
174
+ encryptKeyId: 'updatedKeyId',
175
+ encryptMeta: '{"key":"updatedValue"}',
176
+ syncVersion: 2,
177
+ deleted: true,
178
+ });
179
+ const updatedFile = filesService.update('1', fileUpdate);
180
+
181
+ expect(updatedFile).toEqual(
182
+ new File({
183
+ id: '1',
184
+ name: 'updatedFile1',
185
+ groupId: 'updatedGroup1',
186
+ encryptSalt: 'updatedSalt',
187
+ encryptTest: 'updatedTest',
188
+ encryptMeta: '{"key":"updatedValue"}',
189
+ encryptKeyId: 'updatedKeyId',
190
+ syncVersion: 2,
191
+ deleted: true,
192
+ }),
193
+ );
194
+ });
195
+
196
+ test('find should return only files accessible to the user', () => {
197
+ filesService.set(
198
+ new File({
199
+ id: crypto.randomBytes(16).toString('hex'),
200
+ groupId: 'group2',
201
+ syncVersion: 1,
202
+ name: 'file2',
203
+ encryptMeta: '{"key":"value2"}',
204
+ deleted: false,
205
+ owner: 'genericAdmin',
206
+ }),
207
+ );
208
+
209
+ filesService.set(
210
+ new File({
211
+ id: crypto.randomBytes(16).toString('hex'),
212
+ groupId: 'group2',
213
+ syncVersion: 1,
214
+ name: 'file2',
215
+ encryptMeta: '{"key":"value2"}',
216
+ deleted: false,
217
+ owner: 'genericUser',
218
+ }),
219
+ );
220
+
221
+ expect(filesService.find({ userId: 'genericUser' })).toHaveLength(1);
222
+ expect(
223
+ filesService.find({ userId: 'genericAdmin' }).length,
224
+ ).toBeGreaterThan(1);
225
+ });
226
+
227
+ test.each([['update-group', null]])(
228
+ 'update should modify a single attribute with groupId = $groupId',
229
+ newGroupId => {
230
+ const fileUpdate = new FileUpdate({
231
+ groupId: newGroupId,
232
+ });
233
+ const updatedFile = filesService.update('1', fileUpdate);
234
+
235
+ expect(updatedFile).toEqual(
236
+ new File({
237
+ id: '1',
238
+ name: 'file1',
239
+ groupId: newGroupId,
240
+ syncVersion: 1,
241
+ encryptMeta: '{"key":"value"}',
242
+ encryptSalt: 'salt',
243
+ encryptTest: 'test',
244
+ encryptKeyId: 'keyid',
245
+ deleted: false,
246
+ }),
247
+ );
248
+ },
249
+ );
250
+ });
@@ -0,0 +1,77 @@
1
+ // This is a version representing the internal format of sync
2
+ // messages. When this changes, all sync files need to be reset. We
3
+ // will check this version when syncing and notify the user if they
4
+ // need to reset.
5
+ const SYNC_FORMAT_VERSION = 2;
6
+
7
+ const validateSyncedFile = (groupId, keyId, currentFile) => {
8
+ if (
9
+ currentFile.syncVersion == null ||
10
+ currentFile.syncVersion < SYNC_FORMAT_VERSION
11
+ ) {
12
+ return 'file-old-version';
13
+ }
14
+
15
+ // When resetting sync state, something went wrong. There is no
16
+ // group id and it's awaiting a file to be uploaded.
17
+ if (currentFile.groupId == null) {
18
+ return 'file-needs-upload';
19
+ }
20
+
21
+ // Check to make sure the uploaded file is valid and has been
22
+ // encrypted with the same key it is registered with (this might
23
+ // be wrong if there was an error during the key creation
24
+ // process)
25
+ const uploadedKeyId = currentFile.encryptMeta
26
+ ? JSON.parse(currentFile.encryptMeta).keyId
27
+ : null;
28
+ if (uploadedKeyId !== currentFile.encryptKeyId) {
29
+ return 'file-key-mismatch';
30
+ }
31
+
32
+ // The changes being synced are part of an old group, which
33
+ // means the file has been reset. User needs to re-download.
34
+ if (groupId !== currentFile.groupId) {
35
+ return 'file-has-reset';
36
+ }
37
+
38
+ // The data is encrypted with a different key which is
39
+ // unacceptable. We can't accept these changes. Reject them and
40
+ // tell the user that they need to generate the correct key
41
+ // (which necessitates a sync reset so they need to re-download).
42
+ if (keyId !== currentFile.encryptKeyId) {
43
+ return 'file-has-new-key';
44
+ }
45
+
46
+ return null;
47
+ };
48
+
49
+ const validateUploadedFile = (groupId, keyId, currentFile) => {
50
+ if (!currentFile) {
51
+ // File is new, so no need to validate
52
+ return null;
53
+ }
54
+ // The uploading file is part of an old group, so reject
55
+ // it. All of its internal sync state is invalid because its
56
+ // old. The sync state has been reset, so user needs to
57
+ // either reset again or download from the current group.
58
+ if (groupId !== currentFile.groupId) {
59
+ return 'file-has-reset';
60
+ }
61
+
62
+ // The key that the file is encrypted with is different than
63
+ // the current registered key. All data must always be
64
+ // encrypted with the registered key for consistency. Key
65
+ // changes always necessitate a sync reset, which means this
66
+ // upload is trying to overwrite another reset. That might
67
+ // be be fine, but since we definitely cannot accept a file
68
+ // encrypted with the wrong key, we bail and suggest the
69
+ // user download the latest file.
70
+ if (keyId !== currentFile.encryptKeyId) {
71
+ return 'file-has-new-key';
72
+ }
73
+
74
+ return null;
75
+ };
76
+
77
+ export { validateSyncedFile, validateUploadedFile };