cozy-client 34.11.0 → 35.0.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.
@@ -548,7 +548,7 @@ var CozyClient = /*#__PURE__*/function () {
548
548
  * Emits
549
549
  *
550
550
  * - "beforeLogout" at the beginning, before links have been reset
551
- * - "login" when the client is fully logged out and links have been reset
551
+ * - "logout" when the client is fully logged out and links have been reset
552
552
  *
553
553
  * @returns {Promise} - Resolves when all links have been reset and client is fully logged out
554
554
  */
@@ -2669,7 +2669,7 @@ var CozyClient = /*#__PURE__*/function () {
2669
2669
  CozyClient.hooks = CozyClient.hooks || {};
2670
2670
  CozyClient.fetchPolicies = _policies.default; //COZY_CLIENT_VERSION_PACKAGE in replaced by babel. See babel config
2671
2671
 
2672
- CozyClient.version = "34.11.0";
2672
+ CozyClient.version = "35.0.0";
2673
2673
 
2674
2674
  _microee.default.mixin(CozyClient);
2675
2675
 
package/dist/index.js CHANGED
@@ -323,7 +323,7 @@ var _registry = _interopRequireDefault(require("./registry"));
323
323
 
324
324
  var _RealTimeQueries = _interopRequireDefault(require("./RealTimeQueries"));
325
325
 
326
- var manifest = _interopRequireWildcard(require("./manifest"));
326
+ var manifest = _interopRequireWildcard(require("./models/manifest"));
327
327
 
328
328
  exports.manifest = manifest;
329
329
 
@@ -221,7 +221,7 @@ var _store = require("./store");
221
221
 
222
222
  var _registry = _interopRequireDefault(require("./registry"));
223
223
 
224
- var manifest = _interopRequireWildcard(require("./manifest"));
224
+ var manifest = _interopRequireWildcard(require("./models/manifest"));
225
225
 
226
226
  exports.manifest = manifest;
227
227
 
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.sanitizeCategories = sanitizeCategories;
9
+ exports.areTermsValid = areTermsValid;
10
+ exports.isPartnershipValid = isPartnershipValid;
11
+ exports.sanitize = sanitize;
12
+ exports.getIdentifier = exports.sanitizeIdentifier = exports.legacyLoginFields = exports.ROLE_IDENTIFIER = void 0;
13
+
14
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
15
+
16
+ var _flow2 = _interopRequireDefault(require("lodash/flow"));
17
+
18
+ var _cloneDeep2 = _interopRequireDefault(require("lodash/cloneDeep"));
19
+
20
+ var _findKey = _interopRequireDefault(require("lodash/findKey"));
21
+
22
+ function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
23
+
24
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
25
+
26
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
27
+
28
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
29
+
30
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
31
+
32
+ var ROLE_IDENTIFIER = 'identifier';
33
+ /**
34
+ * Legacy login fields declared by some konnectors
35
+ */
36
+
37
+ exports.ROLE_IDENTIFIER = ROLE_IDENTIFIER;
38
+ var legacyLoginFields = ['login', 'identifier', 'new_identifier', 'email'];
39
+ exports.legacyLoginFields = legacyLoginFields;
40
+ var legacyEncryptedFields = ['secret', 'dob', 'code', 'answer', 'access_token', 'refresh_token', 'appSecret'];
41
+ var APP_CATEGORIES = ['banking', 'cozy', 'energy', 'health', 'host_provider', 'insurance', 'isp', 'mes_infos', 'online_services', 'others', 'partners', 'press', 'productivity', 'ptnb', 'public_service', 'shopping', 'social', 'telecom', 'transport'];
42
+ /**
43
+ * Filters unauthorized categories. Defaults to ['others'] if no suitable category.
44
+ *
45
+ * @param {Array<Object>} categories - Array of categories
46
+ * @returns {Array<Object>} sanitized categories
47
+ */
48
+
49
+ function sanitizeCategories(categories) {
50
+ if (!categories) return ['others'];
51
+ var filteredList = categories.filter(function (c) {
52
+ return APP_CATEGORIES.includes(c);
53
+ });
54
+ if (!filteredList.length) return ['others'];
55
+ return filteredList;
56
+ }
57
+
58
+ function areTermsValid(terms) {
59
+ return Boolean(terms && terms.id && terms.url && terms.version);
60
+ }
61
+
62
+ function isPartnershipValid(partnership) {
63
+ return Boolean(partnership && partnership.description);
64
+ }
65
+ /**
66
+ * Normalize app manifest, retro-compatibility for old manifests
67
+ *
68
+ * @param {import('../types').Manifest} manifest - app manifest to normalize
69
+ * @returns {import('../types').SanitizedManifest}
70
+ */
71
+
72
+
73
+ function sanitize(manifest) {
74
+ var sanitized = _objectSpread({}, manifest); // Make categories an array and delete category attribute if it exists
75
+
76
+
77
+ if (!manifest.categories && manifest.category && typeof manifest.category === 'string') {
78
+ sanitized.categories = [manifest.category];
79
+ delete sanitized.category;
80
+ }
81
+
82
+ sanitized.categories = sanitizeCategories(sanitized.categories); // manifest name is not an object
83
+
84
+ if (typeof manifest.name === 'object') sanitized.name = manifest.name.en; // Fix camelCase from cozy-stack
85
+
86
+ if (manifest.available_version) {
87
+ sanitized.availableVersion = manifest.available_version;
88
+ delete sanitized.available_version;
89
+ } // Fix camelCase from cozy-stack
90
+
91
+
92
+ if (manifest.latest_version) {
93
+ sanitized.latestVersion = manifest.latestVersion;
94
+ delete sanitized.latest_version;
95
+ } // Remove invalid terms
96
+
97
+
98
+ if (sanitized.terms && !areTermsValid(sanitized.terms)) {
99
+ delete sanitized.terms;
100
+ } // Remove invalid partnership
101
+
102
+
103
+ if (sanitized.partnership && !isPartnershipValid(sanitized.partnership)) {
104
+ delete sanitized.partnership;
105
+ }
106
+
107
+ if (sanitized.fields) {
108
+ sanitized.fields = sanitizeFields(manifest.fields);
109
+ }
110
+
111
+ return sanitized;
112
+ }
113
+ /**
114
+ * Ensures that fields has at least one field with the role 'identifier'
115
+ *
116
+ * @param {import('../types').ManifestFields} fields - Manifest fields
117
+ * @returns {import('../types').ManifestFields} - Sanitized manifest fields
118
+ */
119
+
120
+
121
+ var sanitizeIdentifier = function sanitizeIdentifier(fields) {
122
+ var sanitized = (0, _cloneDeep2.default)(fields);
123
+ var hasIdentifier = false;
124
+
125
+ for (var fieldName in sanitized) {
126
+ if (sanitized[fieldName].role === ROLE_IDENTIFIER) {
127
+ if (hasIdentifier) delete sanitized[fieldName].role;else hasIdentifier = true;
128
+ }
129
+ }
130
+
131
+ if (hasIdentifier) return sanitized;
132
+
133
+ var _iterator = _createForOfIteratorHelper(legacyLoginFields),
134
+ _step;
135
+
136
+ try {
137
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
138
+ var name = _step.value;
139
+
140
+ if (sanitized[name]) {
141
+ sanitized[name].role = ROLE_IDENTIFIER;
142
+ return sanitized;
143
+ }
144
+ }
145
+ } catch (err) {
146
+ _iterator.e(err);
147
+ } finally {
148
+ _iterator.f();
149
+ }
150
+
151
+ for (var _fieldName in sanitized) {
152
+ if (sanitized[_fieldName].type !== 'password') {
153
+ sanitized[_fieldName].role = ROLE_IDENTIFIER;
154
+ return sanitized;
155
+ }
156
+ }
157
+
158
+ return sanitized;
159
+ };
160
+ /**
161
+ * Returns the key for the field having the role=identifier attribute
162
+ *
163
+ * @param {import('../types').ManifestFields} fields Konnector fields
164
+ * @returns {String|null} The key for the identifier field, example 'login'
165
+ */
166
+
167
+
168
+ exports.sanitizeIdentifier = sanitizeIdentifier;
169
+
170
+ var getIdentifier = function getIdentifier() {
171
+ var fields = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
172
+ return (0, _findKey.default)(sanitizeIdentifier(fields), function (field) {
173
+ return field.role === ROLE_IDENTIFIER;
174
+ }) || null;
175
+ };
176
+ /**
177
+ * Ensures old fields are removed
178
+ *
179
+ * @param {Object} fields Manifest fields
180
+ * @returns {Object} Sanitized manifest fields
181
+ */
182
+
183
+
184
+ exports.getIdentifier = getIdentifier;
185
+
186
+ var removeOldFields = function removeOldFields(fields) {
187
+ var sanitized = (0, _cloneDeep2.default)(fields);
188
+ delete sanitized.advancedFields;
189
+ return sanitized;
190
+ };
191
+ /**
192
+ * Ensures every field not explicitely tagged as not required is required
193
+ *
194
+ * @param {import('../types').ManifestFields} [fields={}] Manifest fields
195
+ * @returns {import('../types').ManifestFields} Sanitized manifest fields
196
+ */
197
+
198
+
199
+ var sanitizeRequired = function sanitizeRequired(fields) {
200
+ var sanitized = (0, _cloneDeep2.default)(fields);
201
+
202
+ for (var fieldName in sanitized) {
203
+ var field = sanitized[fieldName]; // Ensure legacy for field isRequired
204
+
205
+ var required = typeof field.required === 'undefined' ? field.isRequired : field.required;
206
+ sanitized[fieldName].required = typeof required === 'boolean' ? required : true;
207
+ }
208
+
209
+ return sanitized;
210
+ };
211
+ /**
212
+ * Ensures:
213
+ * * any field flagged as encrypted keeps its flag
214
+ * * any legacy encrypted field is tagged as encrypted
215
+ *
216
+ * @param {import('../types').ManifestFields} [fields={}] Manifest fields
217
+ * @returns {import('../types').ManifestFields} Sanitized Manifest fields
218
+ */
219
+
220
+
221
+ var sanitizeEncrypted = function sanitizeEncrypted(fields) {
222
+ var sanitized = (0, _cloneDeep2.default)(fields);
223
+
224
+ for (var fieldName in sanitized) {
225
+ var field = sanitized[fieldName];
226
+ if (typeof field.encrypted !== 'boolean') field.encrypted = field.type === 'password' || legacyEncryptedFields.includes(fieldName);
227
+ }
228
+
229
+ return sanitized;
230
+ };
231
+ /**
232
+ * Sanitizes manifest fields with multiple rules
233
+ *
234
+ * @param {import('../types').ManifestFields} [fields={}] Manifest fields
235
+ * @returns {import('../types').ManifestFields} Sanitized manifest fields
236
+ */
237
+
238
+
239
+ var sanitizeFields = (0, _flow2.default)([removeOldFields, sanitizeIdentifier, sanitizeRequired, sanitizeEncrypted]);
@@ -0,0 +1,446 @@
1
+ import {
2
+ sanitizeCategories,
3
+ sanitize as sanitizeManifest,
4
+ getIdentifier
5
+ } from './manifest'
6
+
7
+ describe('sanitizeCategories', () => {
8
+ it('should return the list of the provided expected categories correctly', () => {
9
+ // all of these categories is authorized by config/categories.json
10
+ // so it won't be filtered
11
+ const categories = [
12
+ 'cozy',
13
+ 'isp',
14
+ 'partners',
15
+ 'press',
16
+ 'shopping',
17
+ 'telecom'
18
+ ]
19
+ expect(sanitizeCategories(categories)).toEqual(categories)
20
+ })
21
+ it('should filter unwanted categories', () => {
22
+ // pressingggg is not allowed as category
23
+ const categories = [
24
+ 'cozy',
25
+ 'isp',
26
+ 'partners',
27
+ 'pressingggg',
28
+ 'shopping',
29
+ 'telecom'
30
+ ]
31
+ expect(sanitizeCategories(categories)).toEqual(
32
+ categories.filter(c => c !== 'pressingggg')
33
+ )
34
+ })
35
+ it('should return "others" as category if no categories provided', () => {
36
+ expect(sanitizeCategories([])).toEqual(['others'])
37
+ expect(sanitizeCategories(null)).toEqual(['others'])
38
+ })
39
+ it('should return "others" as category if categories after filtering is empty', () => {
40
+ // pressingggg is not allowed as category
41
+ expect(sanitizeCategories(['pressingggg'])).toEqual(['others'])
42
+ })
43
+ })
44
+
45
+ describe('sanitizeManifest', () => {
46
+ const baseMan = { slug: 'mock' }
47
+
48
+ it('should handle legacy category property', () => {
49
+ const testCategory = 'cozy'
50
+ expect(
51
+ sanitizeManifest({ slug: 'mock', category: testCategory })
52
+ ).toStrictEqual({
53
+ slug: 'mock',
54
+ categories: [testCategory]
55
+ })
56
+ })
57
+ it('should handle legacy name property locales ("en" only)', () => {
58
+ const testName = 'Mock'
59
+ expect(
60
+ sanitizeManifest({
61
+ slug: 'mock',
62
+ name: {
63
+ en: testName
64
+ }
65
+ })
66
+ ).toEqual(
67
+ expect.objectContaining({
68
+ slug: 'mock',
69
+ name: testName
70
+ })
71
+ )
72
+ })
73
+
74
+ it('should transform available_version property to availableVersion', () => {
75
+ const testVersion = '3.0.0'
76
+ const man = sanitizeManifest({
77
+ slug: 'mock',
78
+ available_version: testVersion
79
+ })
80
+ expect(man.availableVersion).toBe('3.0.0')
81
+ expect(man).not.toHaveProperty('available_version')
82
+ })
83
+
84
+ it('should remove "fields" if fields is null', () => {
85
+ const current = {
86
+ fields: null
87
+ }
88
+ const result = sanitizeManifest(current)
89
+ expect(result.fields).toBe(null)
90
+ })
91
+
92
+ it("shouldn't modify if fields is undefined", () => {
93
+ const current = {}
94
+ const result = sanitizeManifest(current)
95
+ expect(result.fields).toBe(undefined)
96
+ })
97
+
98
+ it('should not mutate source manifest', () => {
99
+ const current = {
100
+ fields: {
101
+ username: {
102
+ type: 'text',
103
+ required: {
104
+ value: 'true'
105
+ }
106
+ },
107
+ passphrase: {
108
+ type: 'password'
109
+ },
110
+ birthdate: {
111
+ type: 'date'
112
+ }
113
+ }
114
+ }
115
+
116
+ sanitizeManifest(current)
117
+ expect(current).toEqual({
118
+ fields: {
119
+ username: {
120
+ type: 'text',
121
+ required: {
122
+ value: 'true'
123
+ }
124
+ },
125
+ passphrase: {
126
+ type: 'password'
127
+ },
128
+ birthdate: {
129
+ type: 'date'
130
+ }
131
+ }
132
+ })
133
+ })
134
+
135
+ it('should remove old property advancedFields in fields', () => {
136
+ const oldManifest = {
137
+ fields: {
138
+ login: {
139
+ type: 'text'
140
+ },
141
+ password: {
142
+ type: 'password'
143
+ },
144
+ advancedFields: {
145
+ folderPath: {
146
+ advanced: true,
147
+ isRequired: false
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ const result = sanitizeManifest(oldManifest)
154
+ expect(result.fields.advancedFields).toBeUndefined()
155
+ })
156
+
157
+ it('should not add any identifier', () => {
158
+ const current = {
159
+ fields: {
160
+ passphrase: {
161
+ required: true,
162
+ type: 'password'
163
+ }
164
+ }
165
+ }
166
+
167
+ const result = sanitizeManifest(current)
168
+ expect(result.fields.passphrase.role).not.toBe('identifier')
169
+ expect(Object.keys(result.fields).length).toBe(1)
170
+ })
171
+
172
+ it('should set role=identifier for login', () => {
173
+ const current = {
174
+ fields: {
175
+ login: { type: 'text' },
176
+ password: { type: 'password' }
177
+ }
178
+ }
179
+ const result = sanitizeManifest(current)
180
+ expect(result.fields.login.role).toBe('identifier')
181
+ })
182
+
183
+ it('should set first non-password field as role=identifier', () => {
184
+ const current = {
185
+ fields: {
186
+ password: { type: 'password' },
187
+ plop: { type: 'text' },
188
+ foo: { type: 'date' }
189
+ }
190
+ }
191
+ const result = sanitizeManifest(current)
192
+ expect(result.fields.plop.role).toBe('identifier')
193
+ expect(result.fields.foo.role).not.toBe('identifier')
194
+ })
195
+
196
+ const legacyLoginFieldsTest = [
197
+ 'login',
198
+ 'identifier',
199
+ 'new_identifier',
200
+ 'email'
201
+ ]
202
+ for (let name of legacyLoginFieldsTest) {
203
+ let inputLegacy = {
204
+ fields: {
205
+ password: { type: 'password' },
206
+ plop: { type: 'text' }
207
+ }
208
+ }
209
+ inputLegacy.fields[name] = { type: 'text' }
210
+ it('should set role=identifier to ' + name, () => {
211
+ const result = sanitizeManifest(inputLegacy)
212
+ expect(result.fields[name].role).toBe('identifier')
213
+ expect(result.fields.plop.role).not.toBe('identifier')
214
+ })
215
+ }
216
+
217
+ it('should set only one identifier', () => {
218
+ const current = {
219
+ fields: {
220
+ identifier: { type: 'text' },
221
+ mail: { type: 'email' },
222
+ login: { type: 'text' },
223
+ new_identifier: { type: 'text' }
224
+ }
225
+ }
226
+ const result = sanitizeManifest(current)
227
+ const identifiers = Object.keys(result.fields).filter(
228
+ name => result.fields[name].role === 'identifier'
229
+ )
230
+ expect(identifiers.length).toBe(1)
231
+ })
232
+
233
+ it('should set only one identifier even if there is many role=identifier in the manifest', () => {
234
+ const current = {
235
+ fields: {
236
+ identifier: { type: 'text' },
237
+ mail: { type: 'email', role: 'identifier' },
238
+ login: { type: 'text', role: 'identifier' },
239
+ new_identifier: { type: 'text', role: 'identifier' }
240
+ }
241
+ }
242
+ const result = sanitizeManifest(current)
243
+ const identifiers = Object.keys(result.fields).filter(
244
+ name => result.fields[name].role === 'identifier'
245
+ )
246
+ expect(identifiers.length).toBe(1)
247
+ })
248
+
249
+ it('should set only one identifier even if there is no password fields', () => {
250
+ const current = {
251
+ fields: {
252
+ plop1: { type: 'text' },
253
+ plop2: { type: 'text' },
254
+ plop3: { type: 'text' },
255
+ plop4: { type: 'text' }
256
+ }
257
+ }
258
+ const result = sanitizeManifest(current)
259
+ const identifiers = Object.keys(result.fields).filter(
260
+ name => result.fields[name].role === 'identifier'
261
+ )
262
+ expect(identifiers.length).toBe(1)
263
+ })
264
+
265
+ it('should keep the identifier priority', () => {
266
+ const current = {
267
+ fields: {
268
+ identifier: { type: 'text' },
269
+ mail: { type: 'email' },
270
+ login: { type: 'text' },
271
+ new_identifier: { type: 'text' }
272
+ }
273
+ }
274
+ const result = sanitizeManifest(current)
275
+ expect(result.fields.login.required).toBe(true)
276
+ })
277
+
278
+ it('should set required=true as default value in fields', () => {
279
+ const current = {
280
+ fields: {
281
+ login: { type: 'text' },
282
+ password: { type: 'password' },
283
+ gender: { type: 'text', required: false },
284
+ country: { type: 'text' }
285
+ }
286
+ }
287
+ const result = sanitizeManifest(current)
288
+ expect(result.fields.login.required).toBe(true)
289
+ expect(result.fields.password.required).toBe(true)
290
+ expect(result.fields.gender.required).toBe(false)
291
+ expect(result.fields.country.required).toBe(true)
292
+ })
293
+
294
+ it('should handle legacy property isRequired in fields', () => {
295
+ const current = {
296
+ fields: {
297
+ login: { type: 'text' },
298
+ password: { type: 'password' },
299
+ gender: { type: 'text', isRequired: false },
300
+ country: { type: 'text' }
301
+ }
302
+ }
303
+ const result = sanitizeManifest(current)
304
+ expect(result.fields.login.required).toBe(true)
305
+ expect(result.fields.password.required).toBe(true)
306
+ expect(result.fields.gender.required).toBe(false)
307
+ expect(result.fields.country.required).toBe(true)
308
+ })
309
+
310
+ it('should set encrypted for type=password fields', () => {
311
+ const current = {
312
+ fields: {
313
+ username: { type: 'text' },
314
+ passphrase: { type: 'password' }
315
+ }
316
+ }
317
+
318
+ const result = sanitizeManifest(current)
319
+ expect(result.fields.passphrase.encrypted).toBe(true)
320
+ })
321
+
322
+ const legacyEncryptedFieldsTest = [
323
+ 'secret',
324
+ 'dob',
325
+ 'code',
326
+ 'answer',
327
+ 'access_token',
328
+ 'refresh_token',
329
+ 'appSecret'
330
+ ]
331
+
332
+ for (let name of legacyEncryptedFieldsTest) {
333
+ let legacyManifest = {
334
+ fields: {
335
+ [name]: { type: 'text' },
336
+ password: { type: 'password' },
337
+ plop: { type: 'text' }
338
+ }
339
+ }
340
+
341
+ it('should set encrypted=true to ' + name, () => {
342
+ const result = sanitizeManifest(legacyManifest)
343
+ expect(result.fields[name].encrypted).toBe(true)
344
+ })
345
+
346
+ it('should keep encrypted value', () => {
347
+ const current = {
348
+ fields: {
349
+ [name]: { type: 'text', encrypted: false },
350
+ passphrase: { type: 'password', encrypted: true }
351
+ }
352
+ }
353
+ const result = sanitizeManifest(current)
354
+ expect(result.fields[name].encrypted).toBe(false)
355
+ expect(result.fields.passphrase.encrypted).toBe(true)
356
+ })
357
+ }
358
+
359
+ describe('terms', () => {
360
+ it('should remove incomplete terms', () => {
361
+ const incompleteTerms = {
362
+ id: 'mock-terms'
363
+ }
364
+ expect(
365
+ sanitizeManifest({ ...baseMan, terms: incompleteTerms })
366
+ ).not.toHaveProperty('terms')
367
+ expect(sanitizeManifest({ ...baseMan, terms: {} })).not.toHaveProperty(
368
+ 'terms'
369
+ )
370
+ })
371
+
372
+ it('should keep complete terms', () => {
373
+ const completeTerms = {
374
+ id: 'mock-terms',
375
+ url: 'mock://terms',
376
+ version: 'mock001'
377
+ }
378
+ const man = { ...baseMan, terms: completeTerms }
379
+
380
+ expect(sanitizeManifest(man)).toEqual(expect.objectContaining(man))
381
+ })
382
+ })
383
+
384
+ describe('partnership', () => {
385
+ it('should remove incomplete', () => {
386
+ const incompletePartnership = {
387
+ icon: 'icon.svg' // icon is optional
388
+ }
389
+
390
+ expect(
391
+ sanitizeManifest({
392
+ ...baseMan,
393
+ partnership: {}
394
+ })
395
+ ).not.toHaveProperty('partnership')
396
+
397
+ expect(
398
+ sanitizeManifest({
399
+ ...baseMan,
400
+ partnership: incompletePartnership
401
+ })
402
+ ).not.toHaveProperty('partnership')
403
+ })
404
+
405
+ it('should keep complete', () => {
406
+ const completePartnership = {
407
+ description: 'A partnership text here' // description is mandatory
408
+ }
409
+
410
+ const man = { ...baseMan, partnership: completePartnership }
411
+ expect(sanitizeManifest(man)).toEqual(expect.objectContaining(man))
412
+ })
413
+ })
414
+ })
415
+
416
+ describe('getIdentifier', () => {
417
+ it('should return field having role=identifier', () => {
418
+ const fields = {
419
+ username: {
420
+ type: 'text'
421
+ },
422
+ id: {
423
+ type: 'text',
424
+ role: 'identifier'
425
+ }
426
+ }
427
+ expect(getIdentifier(fields)).toBe('id')
428
+ })
429
+
430
+ it('should return the first field', () => {
431
+ const fields = {
432
+ username: {
433
+ type: 'text'
434
+ },
435
+ id: {
436
+ type: 'text'
437
+ }
438
+ }
439
+
440
+ expect(getIdentifier(fields)).toBe('username')
441
+ })
442
+
443
+ it('should return null if there is no match', () => {
444
+ expect(getIdentifier({})).toBe(null)
445
+ })
446
+ })
package/dist/types.js CHANGED
@@ -34,6 +34,22 @@ var _dsl = require("./queries/dsl");
34
34
  * @typedef {object} Token
35
35
  * @typedef {object} ClientResponse
36
36
  * @typedef {object} Manifest
37
+ * @typedef {object} SanitizedManifest
38
+ */
39
+
40
+ /**
41
+ * @typedef {Object} ManifestField
42
+ * @property {String} [type] - field type : can be "text" or "hidden" or "date" or "dropdown" or "password"
43
+ * @property {String} [role] - field role : with "identifier" value
44
+ * @property {Boolean} [required] - is the field required or not
45
+ * @property {Boolean} [isRequired] - is the field required or not (legacy)
46
+ * @property {Boolean} [encrypted] - encrypted value of the field (legacy)
47
+ */
48
+
49
+ /**
50
+ * @typedef {{
51
+ * [key: string]: ManifestField;
52
+ * }} ManifestFields
37
53
  */
38
54
 
39
55
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-client",
3
- "version": "34.11.0",
3
+ "version": "35.0.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.node.js",
6
6
  "browser": "dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "@types/jest": "^26.0.20",
19
19
  "@types/lodash": "^4.14.170",
20
20
  "btoa": "^1.2.1",
21
- "cozy-stack-client": "^34.11.0",
21
+ "cozy-stack-client": "^35.0.0",
22
22
  "date-fns": "2.29.3",
23
23
  "json-stable-stringify": "^1.0.1",
24
24
  "lodash": "^4.17.13",
@@ -73,5 +73,5 @@
73
73
  "react-native-ios11-devicecheck": "https://github.com/cozy/react-native-devicecheck#app-attest-v0.1"
74
74
  },
75
75
  "sideEffects": false,
76
- "gitHead": "eddc8bbd475912c0a7de8b136e04b0248c15dd12"
76
+ "gitHead": "261c75d0ee603b66b2da046a9d04d6876a17eec6"
77
77
  }
@@ -284,7 +284,7 @@ declare class CozyClient {
284
284
  * Emits
285
285
  *
286
286
  * - "beforeLogout" at the beginning, before links have been reset
287
- * - "login" when the client is fully logged out and links have been reset
287
+ * - "logout" when the client is fully logged out and links have been reset
288
288
  *
289
289
  * @returns {Promise} - Resolves when all links have been reset and client is fully logged out
290
290
  */
package/types/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { default as fetchPolicies } from "./policies";
13
13
  export * from "./mock";
14
14
  export * from "./hooks";
15
15
  export { BulkEditError } from "./errors";
16
- import * as manifest from "./manifest";
16
+ import * as manifest from "./models/manifest";
17
17
  import * as models from "./models";
18
18
  export { manifest, models };
19
19
  export { QueryDefinition, Q, Mutations, MutationTypes, getDoctypeFromOperation } from "./queries/dsl";
@@ -9,7 +9,7 @@ export * from "./mock";
9
9
  export * from "./cli";
10
10
  export { BulkEditError } from "./errors";
11
11
  export { default as fetchPolicies } from "./policies";
12
- import * as manifest from "./manifest";
12
+ import * as manifest from "./models/manifest";
13
13
  import * as models from "./models";
14
14
  export { manifest, models };
15
15
  export { QueryDefinition, Mutations, MutationTypes, getDoctypeFromOperation, Q } from "./queries/dsl";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Filters unauthorized categories. Defaults to ['others'] if no suitable category.
3
+ *
4
+ * @param {Array<Object>} categories - Array of categories
5
+ * @returns {Array<Object>} sanitized categories
6
+ */
7
+ export function sanitizeCategories(categories: Array<any>): Array<any>;
8
+ export function areTermsValid(terms: any): boolean;
9
+ export function isPartnershipValid(partnership: any): boolean;
10
+ /**
11
+ * Normalize app manifest, retro-compatibility for old manifests
12
+ *
13
+ * @param {import('../types').Manifest} manifest - app manifest to normalize
14
+ * @returns {import('../types').SanitizedManifest}
15
+ */
16
+ export function sanitize(manifest: import('../types').Manifest): import('../types').SanitizedManifest;
17
+ export const ROLE_IDENTIFIER: "identifier";
18
+ /**
19
+ * Legacy login fields declared by some konnectors
20
+ */
21
+ export const legacyLoginFields: string[];
22
+ export function sanitizeIdentifier(fields: import('../types').ManifestFields): import('../types').ManifestFields;
23
+ export function getIdentifier(fields?: import('../types').ManifestFields): string | null;
package/types/types.d.ts CHANGED
@@ -20,6 +20,32 @@ export type ReduxStore = any;
20
20
  export type Token = any;
21
21
  export type ClientResponse = any;
22
22
  export type Manifest = any;
23
+ export type SanitizedManifest = any;
24
+ export type ManifestField = {
25
+ /**
26
+ * - field type : can be "text" or "hidden" or "date" or "dropdown" or "password"
27
+ */
28
+ type?: string;
29
+ /**
30
+ * - field role : with "identifier" value
31
+ */
32
+ role?: string;
33
+ /**
34
+ * - is the field required or not
35
+ */
36
+ required?: boolean;
37
+ /**
38
+ * - is the field required or not (legacy)
39
+ */
40
+ isRequired?: boolean;
41
+ /**
42
+ * - encrypted value of the field (legacy)
43
+ */
44
+ encrypted?: boolean;
45
+ };
46
+ export type ManifestFields = {
47
+ [key: string]: ManifestField;
48
+ };
23
49
  export type OldCozyClient = any;
24
50
  export type NodeEnvironment = any;
25
51
  export type QueryFetchStatus = "failed" | "loading" | "pending" | "loaded";
package/dist/manifest.js DELETED
@@ -1,83 +0,0 @@
1
- "use strict";
2
-
3
- var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
-
5
- Object.defineProperty(exports, "__esModule", {
6
- value: true
7
- });
8
- exports.sanitizeCategories = sanitizeCategories;
9
- exports.areTermsValid = areTermsValid;
10
- exports.isPartnershipValid = isPartnershipValid;
11
- exports.sanitize = sanitize;
12
-
13
- var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
14
-
15
- var _types = require("./types");
16
-
17
- function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
18
-
19
- function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
20
-
21
- var APP_CATEGORIES = ['banking', 'cozy', 'energy', 'health', 'host_provider', 'insurance', 'isp', 'mes_infos', 'online_services', 'others', 'partners', 'press', 'productivity', 'ptnb', 'public_service', 'shopping', 'social', 'telecom', 'transport'];
22
- /** Filters unauthorized categories. Defaults to ['others'] if no suitable category. */
23
-
24
- function sanitizeCategories(categories) {
25
- if (!categories) return ['others'];
26
- var filteredList = categories.filter(function (c) {
27
- return APP_CATEGORIES.includes(c);
28
- });
29
- if (!filteredList.length) return ['others'];
30
- return filteredList;
31
- }
32
-
33
- function areTermsValid(terms) {
34
- return Boolean(terms && terms.id && terms.url && terms.version);
35
- }
36
-
37
- function isPartnershipValid(partnership) {
38
- return Boolean(partnership && partnership.description);
39
- }
40
- /**
41
- * Normalize app manifest, retro-compatibility for old manifests
42
- *
43
- * @param {Manifest} manifest - app manifest to normalize
44
- * @returns {Manifest}
45
- */
46
-
47
-
48
- function sanitize(manifest) {
49
- var sanitized = _objectSpread({}, manifest); // Make categories an array and delete category attribute if it exists
50
-
51
-
52
- if (!manifest.categories && manifest.category && typeof manifest.category === 'string') {
53
- sanitized.categories = [manifest.category];
54
- delete sanitized.category;
55
- }
56
-
57
- sanitized.categories = sanitizeCategories(sanitized.categories); // manifest name is not an object
58
-
59
- if (typeof manifest.name === 'object') sanitized.name = manifest.name.en; // Fix camelCase from cozy-stack
60
-
61
- if (manifest.available_version) {
62
- sanitized.availableVersion = manifest.available_version;
63
- delete sanitized.available_version;
64
- } // Fix camelCase from cozy-stack
65
-
66
-
67
- if (manifest.latest_version) {
68
- sanitized.latestVersion = manifest.latestVersion;
69
- delete sanitized.latest_version;
70
- } // Remove invalid terms
71
-
72
-
73
- if (sanitized.terms && !areTermsValid(sanitized.terms)) {
74
- delete sanitized.terms;
75
- } // Remove invalid partnership
76
-
77
-
78
- if (sanitized.partnership && !isPartnershipValid(sanitized.partnership)) {
79
- delete sanitized.partnership;
80
- }
81
-
82
- return sanitized;
83
- }
@@ -1,135 +0,0 @@
1
- import { sanitizeCategories, sanitize as sanitizeManifest } from './manifest'
2
-
3
- describe('sanitizeCategories', () => {
4
- it('should return the list of the provided expected categories correctly', () => {
5
- // all of these categories is authorized by config/categories.json
6
- // so it won't be filtered
7
- const categories = [
8
- 'cozy',
9
- 'isp',
10
- 'partners',
11
- 'press',
12
- 'shopping',
13
- 'telecom'
14
- ]
15
- expect(sanitizeCategories(categories)).toEqual(categories)
16
- })
17
- it('should filter unwanted categories', () => {
18
- // pressingggg is not allowed as category
19
- const categories = [
20
- 'cozy',
21
- 'isp',
22
- 'partners',
23
- 'pressingggg',
24
- 'shopping',
25
- 'telecom'
26
- ]
27
- expect(sanitizeCategories(categories)).toEqual(
28
- categories.filter(c => c !== 'pressingggg')
29
- )
30
- })
31
- it('should return "others" as category if no categories provided', () => {
32
- expect(sanitizeCategories([])).toEqual(['others'])
33
- expect(sanitizeCategories(null)).toEqual(['others'])
34
- })
35
- it('should return "others" as category if categories after filtering is empty', () => {
36
- // pressingggg is not allowed as category
37
- expect(sanitizeCategories(['pressingggg'])).toEqual(['others'])
38
- })
39
- })
40
-
41
- describe('sanitizeManifest', () => {
42
- const baseMan = { slug: 'mock' }
43
-
44
- it('should handle legacy category property', () => {
45
- const testCategory = 'cozy'
46
- expect(
47
- sanitizeManifest({ slug: 'mock', category: testCategory })
48
- ).toStrictEqual({
49
- slug: 'mock',
50
- categories: [testCategory]
51
- })
52
- })
53
- it('should handle legacy name property locales ("en" only)', () => {
54
- const testName = 'Mock'
55
- expect(
56
- sanitizeManifest({
57
- slug: 'mock',
58
- name: {
59
- en: testName
60
- }
61
- })
62
- ).toEqual(
63
- expect.objectContaining({
64
- slug: 'mock',
65
- name: testName
66
- })
67
- )
68
- })
69
-
70
- it('should transform available_version property to availableVersion', () => {
71
- const testVersion = '3.0.0'
72
- const man = sanitizeManifest({
73
- slug: 'mock',
74
- available_version: testVersion
75
- })
76
- expect(man.availableVersion).toBe('3.0.0')
77
- expect(man).not.toHaveProperty('available_version')
78
- })
79
-
80
- describe('terms', () => {
81
- it('should remove incomplete terms', () => {
82
- const incompleteTerms = {
83
- id: 'mock-terms'
84
- }
85
- expect(
86
- sanitizeManifest({ ...baseMan, terms: incompleteTerms })
87
- ).not.toHaveProperty('terms')
88
- expect(sanitizeManifest({ ...baseMan, terms: {} })).not.toHaveProperty(
89
- 'terms'
90
- )
91
- })
92
-
93
- it('should keep complete terms', () => {
94
- const completeTerms = {
95
- id: 'mock-terms',
96
- url: 'mock://terms',
97
- version: 'mock001'
98
- }
99
- const man = { ...baseMan, terms: completeTerms }
100
-
101
- expect(sanitizeManifest(man)).toEqual(expect.objectContaining(man))
102
- })
103
- })
104
-
105
- describe('partnership', () => {
106
- it('should remove incomplete', () => {
107
- const incompletePartnership = {
108
- icon: 'icon.svg' // icon is optional
109
- }
110
-
111
- expect(
112
- sanitizeManifest({
113
- ...baseMan,
114
- partnership: {}
115
- })
116
- ).not.toHaveProperty('partnership')
117
-
118
- expect(
119
- sanitizeManifest({
120
- ...baseMan,
121
- partnership: incompletePartnership
122
- })
123
- ).not.toHaveProperty('partnership')
124
- })
125
-
126
- it('should keep complete', () => {
127
- const completePartnership = {
128
- description: 'A partnership text here' // description is mandatory
129
- }
130
-
131
- const man = { ...baseMan, partnership: completePartnership }
132
- expect(sanitizeManifest(man)).toEqual(expect.objectContaining(man))
133
- })
134
- })
135
- })
@@ -1,12 +0,0 @@
1
- /** Filters unauthorized categories. Defaults to ['others'] if no suitable category. */
2
- export function sanitizeCategories(categories: any): any;
3
- export function areTermsValid(terms: any): boolean;
4
- export function isPartnershipValid(partnership: any): boolean;
5
- /**
6
- * Normalize app manifest, retro-compatibility for old manifests
7
- *
8
- * @param {Manifest} manifest - app manifest to normalize
9
- * @returns {Manifest}
10
- */
11
- export function sanitize(manifest: Manifest): Manifest;
12
- import { Manifest } from "./types";