@twin.org/synchronised-storage-service 0.0.1-next.3 → 0.0.1-next.4

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 (34) hide show
  1. package/dist/cjs/index.cjs +641 -176
  2. package/dist/esm/index.mjs +642 -178
  3. package/dist/types/entities/syncSnapshotEntry.d.ts +1 -2
  4. package/dist/types/helpers/blobStorageHelper.d.ts +33 -0
  5. package/dist/types/helpers/changeSetHelper.d.ts +19 -7
  6. package/dist/types/helpers/localSyncStateHelper.d.ts +8 -23
  7. package/dist/types/helpers/remoteSyncStateHelper.d.ts +15 -11
  8. package/dist/types/helpers/versions.d.ts +3 -0
  9. package/dist/types/index.d.ts +0 -2
  10. package/dist/types/models/ISyncPointerStore.d.ts +4 -0
  11. package/dist/types/models/ISyncSnapshot.d.ts +5 -1
  12. package/dist/types/models/ISyncState.d.ts +4 -0
  13. package/dist/types/models/ISynchronisedStorageServiceConfig.d.ts +12 -6
  14. package/dist/types/models/ISynchronisedStorageServiceConstructorOptions.d.ts +6 -2
  15. package/dist/types/synchronisedStorageRoutes.d.ts +9 -1
  16. package/dist/types/synchronisedStorageService.d.ts +13 -4
  17. package/docs/architecture.md +125 -0
  18. package/docs/changelog.md +15 -0
  19. package/docs/open-api/spec.json +244 -18
  20. package/docs/reference/classes/SyncSnapshotEntry.md +1 -1
  21. package/docs/reference/classes/SynchronisedStorageService.md +38 -5
  22. package/docs/reference/functions/synchronisedStorageGetDecryptionKeyRequest.md +31 -0
  23. package/docs/reference/index.md +1 -2
  24. package/docs/reference/interfaces/ISyncPointerStore.md +8 -0
  25. package/docs/reference/interfaces/ISyncSnapshot.md +10 -2
  26. package/docs/reference/interfaces/ISyncState.md +8 -0
  27. package/docs/reference/interfaces/ISynchronisedStorageServiceConfig.md +30 -10
  28. package/docs/reference/interfaces/ISynchronisedStorageServiceConstructorOptions.md +11 -3
  29. package/locales/en.json +46 -18
  30. package/package.json +3 -2
  31. package/dist/types/models/ISyncChange.d.ts +0 -18
  32. package/dist/types/models/ISyncChangeSet.d.ts +0 -36
  33. package/docs/reference/interfaces/ISyncChange.md +0 -33
  34. package/docs/reference/interfaces/ISyncChangeSet.md +0 -65
@@ -1,13 +1,15 @@
1
1
  import { property, entity, EntitySchemaFactory, EntitySchemaHelper, ComparisonOperator } from '@twin.org/entity';
2
- import { Guards, ComponentFactory, Is, ObjectHelper, Converter, RandomHelper, BaseError, NotFoundError, GeneralError } from '@twin.org/core';
2
+ import { Guards, ComponentFactory, Is, Converter, Compression, CompressionType, ObjectHelper, BaseError, GeneralError, RandomHelper, NotFoundError, UnauthorizedError } from '@twin.org/core';
3
3
  import { HttpStatusCode } from '@twin.org/web';
4
+ import { BlobStorageConnectorFactory } from '@twin.org/blob-storage-models';
4
5
  import { EntityStorageConnectorFactory } from '@twin.org/entity-storage-models';
5
6
  import { DocumentHelper, IdentityConnectorFactory } from '@twin.org/identity-models';
6
7
  import { LoggingConnectorFactory } from '@twin.org/logging-models';
8
+ import { ProofTypes } from '@twin.org/standards-w3c-did';
7
9
  import { SyncChangeOperation, SynchronisedStorageTopics } from '@twin.org/synchronised-storage-models';
10
+ import { VaultEncryptionType, VaultConnectorFactory } from '@twin.org/vault-models';
8
11
  import { VerifiableStorageConnectorFactory } from '@twin.org/verifiable-storage-models';
9
- import { BlobStorageCompressionType } from '@twin.org/blob-storage-models';
10
- import { ProofTypes } from '@twin.org/standards-w3c-did';
12
+ import { RSA } from '@twin.org/crypto';
11
13
 
12
14
  // Copyright 2024 IOTA Stiftung.
13
15
  // SPDX-License-Identifier: Apache-2.0.
@@ -100,8 +102,8 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
100
102
  operationId: "synchronisedStorageSyncChangeSetRequest",
101
103
  summary: "Request that the node perform a sync request for a changeset.",
102
104
  tag: tagsSynchronisedStorage[0].name,
103
- method: "GET",
104
- path: `${baseRouteName}/`,
105
+ method: "POST",
106
+ path: `${baseRouteName}/sync-changeset`,
105
107
  handler: async (httpRequestContext, request) => synchronisedStorageSyncChangeSetRequest(httpRequestContext, componentName, request),
106
108
  requestType: {
107
109
  type: "ISyncChangeSetRequest",
@@ -109,8 +111,29 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
109
111
  {
110
112
  id: "synchronisedStorageSyncChangeSetRequestExample",
111
113
  request: {
112
- query: {
113
- changeSetStorageId: "12345"
114
+ body: {
115
+ id: "0909090909090909090909090909090909090909090909090909090909090909",
116
+ dateCreated: "2025-05-29T01:00:00.000Z",
117
+ nodeIdentity: "did:entity-storage:0xd2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
118
+ changes: [
119
+ {
120
+ entity: {
121
+ dateModified: "2025-01-01T00:00:00.000Z"
122
+ },
123
+ id: "test-id-1",
124
+ operation: "set"
125
+ }
126
+ ],
127
+ proof: {
128
+ "@context": "https://www.w3.org/ns/credentials/v2",
129
+ created: "2025-05-29T01:00:00.000Z",
130
+ cryptosuite: "eddsa-jcs-2022",
131
+ proofPurpose: "assertionMethod",
132
+ proofValue: "z5efBErQs3YBLZoH7jgKMQaRc9YjAxA5XSYKmW3FmTBDw9WionT2NS2x1SMvcRyBvw53cSSoaCT1xQH9tkWngGCX3",
133
+ type: "DataIntegrityProof",
134
+ verificationMethod: "did:entity-storage:0xd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0#synchronised-storage-assertion"
135
+ },
136
+ storageKey: "test-type"
114
137
  }
115
138
  }
116
139
  }
@@ -120,9 +143,61 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
120
143
  {
121
144
  type: "INoContentResponse"
122
145
  }
123
- ]
146
+ ],
147
+ // Authentication is provided by the proof in the request body.
148
+ skipAuth: true
149
+ };
150
+ const getDecryptionKeyRoute = {
151
+ operationId: "synchronisedStorageGetDecryptionKeyRequest",
152
+ summary: "Request the decryption key.",
153
+ tag: tagsSynchronisedStorage[0].name,
154
+ method: "POST",
155
+ path: `${baseRouteName}/decryption-key`,
156
+ handler: async (httpRequestContext, request) => synchronisedStorageGetDecryptionKeyRequest(httpRequestContext, componentName, request),
157
+ requestType: {
158
+ type: "ISyncChangeSetRequest",
159
+ examples: [
160
+ {
161
+ id: "synchronisedStorageSyncGetDecryptionKeyRequestExample",
162
+ request: {
163
+ body: {
164
+ nodeIdentity: "did:entity-storage:0xd2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
165
+ proof: {
166
+ "@context": "https://www.w3.org/ns/credentials/v2",
167
+ created: "2025-05-29T01:00:00.000Z",
168
+ cryptosuite: "eddsa-jcs-2022",
169
+ proofPurpose: "assertionMethod",
170
+ proofValue: "z5efBErQs3YBLZoH7jgKMQaRc9YjAxA5XSYKmW3FmTBDw9WionT2NS2x1SMvcRyBvw53cSSoaCT1xQH9tkWngGCX3",
171
+ type: "DataIntegrityProof",
172
+ verificationMethod: "did:entity-storage:0xd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0#synchronised-storage-assertion"
173
+ }
174
+ }
175
+ }
176
+ }
177
+ ]
178
+ },
179
+ responseType: [
180
+ {
181
+ type: "ISyncDecryptionKeyResponse",
182
+ examples: [
183
+ {
184
+ id: "synchronisedStorageSyncGetDecryptionKeyResponseExample",
185
+ response: {
186
+ body: {
187
+ decryptionKey: "z5efBErQs3YBLZoH7jgKMQaRc9YjAxA5XSYKmW3FmTBDw9WionT2NS2x1SMvcRyBvw53cSSoaCT1xQH9tkWngGCX3"
188
+ }
189
+ }
190
+ }
191
+ ]
192
+ },
193
+ {
194
+ type: "IUnauthorizedResponse"
195
+ }
196
+ ],
197
+ // Authentication is provided by the proof in the request body.
198
+ skipAuth: true
124
199
  };
125
- return [syncChangeSetRoute];
200
+ return [syncChangeSetRoute, getDecryptionKeyRoute];
126
201
  }
127
202
  /**
128
203
  * Perform the sync change set operation.
@@ -133,13 +208,31 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
133
208
  */
134
209
  async function synchronisedStorageSyncChangeSetRequest(httpRequestContext, componentName, request) {
135
210
  Guards.object(ROUTES_SOURCE, "request", request);
136
- Guards.object(ROUTES_SOURCE, "request.query", request.query);
211
+ Guards.object(ROUTES_SOURCE, "request.body", request.body);
137
212
  const component = ComponentFactory.get(componentName);
138
- await component.syncChangeSet(request.query.changeSetStorageId);
213
+ await component.syncChangeSet(request.body);
139
214
  return {
140
215
  statusCode: HttpStatusCode.noContent
141
216
  };
142
217
  }
218
+ /**
219
+ * Request the decryption key.
220
+ * @param httpRequestContext The request context for the API.
221
+ * @param componentName The name of the component to use in the routes.
222
+ * @param request The request.
223
+ * @returns The response object with additional http response properties.
224
+ */
225
+ async function synchronisedStorageGetDecryptionKeyRequest(httpRequestContext, componentName, request) {
226
+ Guards.object(ROUTES_SOURCE, "request", request);
227
+ Guards.object(ROUTES_SOURCE, "request.body", request.body);
228
+ const component = ComponentFactory.get(componentName);
229
+ const key = await component.getDecryptionKey(request.body.nodeIdentity, request.body.proof);
230
+ return {
231
+ body: {
232
+ decryptionKey: key
233
+ }
234
+ };
235
+ }
143
236
 
144
237
  const restEntryPoints = [
145
238
  {
@@ -159,6 +252,163 @@ function initSchema() {
159
252
  EntitySchemaFactory.register("SyncSnapshotEntry", () => EntitySchemaHelper.getSchema(SyncSnapshotEntry));
160
253
  }
161
254
 
255
+ var mainnet = "";
256
+ var testnet = "";
257
+ var devnet = "";
258
+ var verifiableStorageKeys = {
259
+ mainnet: mainnet,
260
+ testnet: testnet,
261
+ devnet: devnet
262
+ };
263
+
264
+ /**
265
+ * Class for performing blob storage operations.
266
+ */
267
+ class BlobStorageHelper {
268
+ /**
269
+ * Runtime name for the class.
270
+ */
271
+ CLASS_NAME = "BlobStorageHelper";
272
+ /**
273
+ * The logging connector to use for logging.
274
+ * @internal
275
+ */
276
+ _logging;
277
+ /**
278
+ * The vault connector.
279
+ * @internal
280
+ */
281
+ _vaultConnector;
282
+ /**
283
+ * The blob storage connector to use.
284
+ * @internal
285
+ */
286
+ _blobStorageConnector;
287
+ /**
288
+ * The id of the vault key to use for encrypting/decrypting blobs.
289
+ * @internal
290
+ */
291
+ _blobStorageEncryptionKeyId;
292
+ /**
293
+ * Is this a trusted node.
294
+ * @internal
295
+ */
296
+ _isTrustedNode;
297
+ /**
298
+ * Create a new instance of BlobStorageHelper.
299
+ * @param logging The logging connector to use for logging.
300
+ * @param vaultConnector The vault connector to use for for the encryption key.
301
+ * @param blobStorageConnector The blob storage component to use.
302
+ * @param blobStorageEncryptionKeyId The id of the vault key to use for encrypting/decrypting blobs.
303
+ * @param isTrustedNode Is this a trusted node.
304
+ */
305
+ constructor(logging, vaultConnector, blobStorageConnector, blobStorageEncryptionKeyId, isTrustedNode) {
306
+ this._logging = logging;
307
+ this._vaultConnector = vaultConnector;
308
+ this._blobStorageConnector = blobStorageConnector;
309
+ this._blobStorageEncryptionKeyId = blobStorageEncryptionKeyId;
310
+ this._isTrustedNode = isTrustedNode;
311
+ }
312
+ /**
313
+ * Load a blob from storage.
314
+ * @param blobId The id of the blob to apply.
315
+ * @returns The blob.
316
+ */
317
+ async load(blobId) {
318
+ await this._logging?.log({
319
+ level: "info",
320
+ source: this.CLASS_NAME,
321
+ message: "loadBlob",
322
+ data: {
323
+ blobId
324
+ }
325
+ });
326
+ try {
327
+ const encryptedBlob = await this._blobStorageConnector.get(blobId);
328
+ if (Is.uint8Array(encryptedBlob)) {
329
+ let compressedBlob;
330
+ // If this is a trusted node, we can decrypt the blob using the vault
331
+ if (this._isTrustedNode) {
332
+ compressedBlob = await this._vaultConnector.decrypt(this._blobStorageEncryptionKeyId, VaultEncryptionType.Rsa2048, encryptedBlob);
333
+ }
334
+ else {
335
+ // Otherwise we need the public key stored as a secret in the vault
336
+ const key = await this._vaultConnector.getSecret(this._blobStorageEncryptionKeyId);
337
+ const rsa = new RSA(Converter.base64ToBytes(key));
338
+ compressedBlob = rsa.decrypt(encryptedBlob);
339
+ }
340
+ const decompressedBlob = await Compression.decompress(compressedBlob, CompressionType.Gzip);
341
+ await this._logging?.log({
342
+ level: "info",
343
+ source: this.CLASS_NAME,
344
+ message: "loadedBlob",
345
+ data: {
346
+ blobId
347
+ }
348
+ });
349
+ return ObjectHelper.fromBytes(decompressedBlob);
350
+ }
351
+ }
352
+ catch (error) {
353
+ await this._logging?.log({
354
+ level: "error",
355
+ source: this.CLASS_NAME,
356
+ message: "loadBlobFailed",
357
+ data: {
358
+ blobId
359
+ },
360
+ error: BaseError.fromError(error)
361
+ });
362
+ }
363
+ await this._logging?.log({
364
+ level: "info",
365
+ source: this.CLASS_NAME,
366
+ message: "loadBlobEmpty",
367
+ data: {
368
+ blobId
369
+ }
370
+ });
371
+ }
372
+ /**
373
+ * Save a blob.
374
+ * @param blob The blob to save.
375
+ * @returns The id of the blob.
376
+ */
377
+ async saveBlob(blob) {
378
+ await this._logging?.log({
379
+ level: "info",
380
+ source: this.CLASS_NAME,
381
+ message: "saveBlob"
382
+ });
383
+ if (!this._isTrustedNode) {
384
+ throw new GeneralError(this.CLASS_NAME, "notTrustedNode");
385
+ }
386
+ const compressedBlob = await Compression.compress(ObjectHelper.toBytes(blob), CompressionType.Gzip);
387
+ const encryptedBlob = await this._vaultConnector.encrypt(this._blobStorageEncryptionKeyId, VaultEncryptionType.Rsa2048, compressedBlob);
388
+ try {
389
+ const blobId = await this._blobStorageConnector.set(encryptedBlob);
390
+ await this._logging?.log({
391
+ level: "info",
392
+ source: this.CLASS_NAME,
393
+ message: "savedBlob",
394
+ data: {
395
+ blobId
396
+ }
397
+ });
398
+ return blobId;
399
+ }
400
+ catch (error) {
401
+ await this._logging?.log({
402
+ level: "error",
403
+ source: this.CLASS_NAME,
404
+ message: "saveBlobFailed",
405
+ error: BaseError.fromError(error)
406
+ });
407
+ throw error;
408
+ }
409
+ }
410
+ }
411
+
162
412
  // Copyright 2024 IOTA Stiftung.
163
413
  // SPDX-License-Identifier: Apache-2.0.
164
414
  /**
@@ -180,10 +430,10 @@ class ChangeSetHelper {
180
430
  */
181
431
  _eventBusComponent;
182
432
  /**
183
- * The blob storage component to use for remote sync states.
433
+ * The blob storage helper to use for remote sync states.
184
434
  * @internal
185
435
  */
186
- _blobStorageComponent;
436
+ _blobStorageHelper;
187
437
  /**
188
438
  * The identity connector to use for signing/verifying changesets.
189
439
  * @internal
@@ -194,37 +444,73 @@ class ChangeSetHelper {
194
444
  * @internal
195
445
  */
196
446
  _decentralisedStorageMethodId;
447
+ /**
448
+ * The identity of the node that is performing the update.
449
+ * @internal
450
+ */
451
+ _nodeIdentity;
197
452
  /**
198
453
  * Create a new instance of ChangeSetHelper.
199
454
  * @param logging The logging connector to use for logging.
200
455
  * @param eventBusComponent The event bus component to use for events.
201
- * @param blobStorageComponent The blob storage component to use for remote sync states.
202
456
  * @param identityConnector The identity connector to use for signing/verifying changesets.
457
+ * @param blobStorageHelper The blob storage component to use for remote sync states.
203
458
  * @param decentralisedStorageMethodId The id of the identity method to use when signing/verifying changesets.
204
459
  */
205
- constructor(logging, eventBusComponent, blobStorageComponent, identityConnector, decentralisedStorageMethodId) {
460
+ constructor(logging, eventBusComponent, identityConnector, blobStorageHelper, decentralisedStorageMethodId) {
206
461
  this._logging = logging;
207
462
  this._eventBusComponent = eventBusComponent;
208
463
  this._decentralisedStorageMethodId = decentralisedStorageMethodId;
209
- this._blobStorageComponent = blobStorageComponent;
464
+ this._blobStorageHelper = blobStorageHelper;
210
465
  this._identityConnector = identityConnector;
211
466
  }
467
+ /**
468
+ * Set the node identity to use for signing changesets.
469
+ * @param nodeIdentity The identity of the node that is performing the update.
470
+ */
471
+ setNodeIdentity(nodeIdentity) {
472
+ this._nodeIdentity = nodeIdentity;
473
+ }
212
474
  /**
213
475
  * Get and verify a changeset.
214
476
  * @param changeSetStorageId The id of the sync changeset to apply.
215
477
  * @returns The changeset if it was verified.
216
478
  */
217
479
  async getAndVerifyChangeset(changeSetStorageId) {
218
- // Changesets are not encrypted as they are signed with the node identity
219
- // and they are publicly accessible so that other nodes can retrieve them.
220
- const blobEntry = await this._blobStorageComponent.get(changeSetStorageId, {
221
- includeContent: true
480
+ await this._logging?.log({
481
+ level: "info",
482
+ source: this.CLASS_NAME,
483
+ message: "getChangeSet",
484
+ data: {
485
+ changeSetStorageId
486
+ }
222
487
  });
223
- if (Is.stringBase64(blobEntry.blob)) {
224
- const syncChangeset = ObjectHelper.fromBytes(Converter.base64ToBytes(blobEntry.blob));
225
- const verified = await this.verifyChangesetProof(syncChangeset);
226
- return verified ? syncChangeset : undefined;
488
+ try {
489
+ const syncChangeSet = await this._blobStorageHelper.load(changeSetStorageId);
490
+ if (Is.object(syncChangeSet)) {
491
+ const verified = await this.verifyChangesetProof(syncChangeSet);
492
+ return verified ? syncChangeSet : undefined;
493
+ }
227
494
  }
495
+ catch (error) {
496
+ await this._logging?.log({
497
+ level: "warn",
498
+ source: this.CLASS_NAME,
499
+ message: "getChangeSetError",
500
+ data: {
501
+ changeSetStorageId
502
+ },
503
+ error: BaseError.fromError(error)
504
+ });
505
+ }
506
+ await this._logging?.log({
507
+ level: "info",
508
+ source: this.CLASS_NAME,
509
+ message: "getChangeSetEmpty",
510
+ data: {
511
+ changeSetStorageId
512
+ }
513
+ });
228
514
  }
229
515
  /**
230
516
  * Apply a sync changeset.
@@ -258,13 +544,18 @@ class ChangeSetHelper {
258
544
  switch (change.operation) {
259
545
  case SyncChangeOperation.Set:
260
546
  if (!Is.empty(change.entity)) {
261
- // The node identity was stripped when stored in the changeset
547
+ // The id was stripped from the entity as it is part of the operation
548
+ // we make sure we reinstate it in the publish
549
+ // Also the node identity was stripped when stored in the changeset
262
550
  // as the changeset is signed with the node identity.
263
551
  // so we need to restore it here.
264
- change.entity.nodeIdentity = syncChangeset.nodeIdentity;
265
552
  await this._eventBusComponent.publish(SynchronisedStorageTopics.RemoteItemSet, {
266
553
  storageKey: syncChangeset.storageKey,
267
- entity: change.entity
554
+ entity: {
555
+ ...change.entity,
556
+ id: change.id,
557
+ nodeIdentity: syncChangeset.nodeIdentity
558
+ }
268
559
  });
269
560
  }
270
561
  break;
@@ -283,10 +574,9 @@ class ChangeSetHelper {
283
574
  /**
284
575
  * Store the changeset.
285
576
  * @param syncChangeSet The sync change set to store.
286
- * @param nodeIdentity The node identity to use for the changeset.
287
577
  * @returns The id of the change set.
288
578
  */
289
- async storeChangeSet(syncChangeSet, nodeIdentity) {
579
+ async storeChangeSet(syncChangeSet) {
290
580
  await this._logging?.log({
291
581
  level: "info",
292
582
  source: this.CLASS_NAME,
@@ -295,12 +585,7 @@ class ChangeSetHelper {
295
585
  id: syncChangeSet.id
296
586
  }
297
587
  });
298
- // We don't want to encrypt the sync state as no other nodes would be able to read it
299
- // the blob storage also needs to be publicly accessible so that other nodes can retrieve it
300
- return this._blobStorageComponent.create(Converter.bytesToBase64(ObjectHelper.toBytes(syncChangeSet)), undefined, undefined, undefined, {
301
- disableEncryption: true,
302
- compress: BlobStorageCompressionType.Gzip
303
- }, undefined, nodeIdentity);
588
+ return this._blobStorageHelper.saveBlob(syncChangeSet);
304
589
  }
305
590
  /**
306
591
  * Verify the proof of a sync changeset.
@@ -319,6 +604,32 @@ class ChangeSetHelper {
319
604
  });
320
605
  return false;
321
606
  }
607
+ // If the proof or verification method is missing, the proof is invalid
608
+ const verificationMethod = syncChangeset.proof?.verificationMethod;
609
+ if (!Is.stringValue(verificationMethod)) {
610
+ await this._logging?.log({
611
+ level: "error",
612
+ source: this.CLASS_NAME,
613
+ message: "verifyChangeSetProofMissing",
614
+ data: {
615
+ id: syncChangeset.id
616
+ }
617
+ });
618
+ }
619
+ // Parse the verification method and extract the node identity
620
+ // this should match the node identity of the changeset
621
+ // otherwise you could sign a changeset for another node
622
+ const changeSetNodeIdentity = DocumentHelper.parseId(verificationMethod ?? "");
623
+ if (changeSetNodeIdentity.id !== syncChangeset.nodeIdentity) {
624
+ await this._logging?.log({
625
+ level: "error",
626
+ source: this.CLASS_NAME,
627
+ message: "verifyChangeSetProofNodeIdentityMismatch",
628
+ data: {
629
+ id: syncChangeset.id
630
+ }
631
+ });
632
+ }
322
633
  const changeSetWithoutProof = ObjectHelper.clone(syncChangeset);
323
634
  delete changeSetWithoutProof.proof;
324
635
  const isValid = await this._identityConnector.verifyProof(changeSetWithoutProof, syncChangeset.proof);
@@ -350,9 +661,10 @@ class ChangeSetHelper {
350
661
  * @returns The proof.
351
662
  */
352
663
  async createChangeSetProof(syncChangeset) {
664
+ Guards.stringValue(this.CLASS_NAME, "nodeIdentity", this._nodeIdentity);
353
665
  const changeSetWithoutProof = ObjectHelper.clone(syncChangeset);
354
666
  delete changeSetWithoutProof.proof;
355
- const proof = await this._identityConnector.createProof(syncChangeset.nodeIdentity, DocumentHelper.joinId(syncChangeset.nodeIdentity, this._decentralisedStorageMethodId), ProofTypes.DataIntegrityProof, changeSetWithoutProof);
667
+ const proof = await this._identityConnector.createProof(this._nodeIdentity, DocumentHelper.joinId(this._nodeIdentity, this._decentralisedStorageMethodId), ProofTypes.DataIntegrityProof, changeSetWithoutProof);
356
668
  await this._logging?.log({
357
669
  level: "info",
358
670
  source: this.CLASS_NAME,
@@ -364,6 +676,35 @@ class ChangeSetHelper {
364
676
  });
365
677
  return proof;
366
678
  }
679
+ /**
680
+ * Copy a change set.
681
+ * @param syncChangeSet The sync changeset to copy.
682
+ * @returns The id of the updated change set.
683
+ */
684
+ async copyChangeset(syncChangeSet) {
685
+ if (Is.stringValue(this._nodeIdentity)) {
686
+ const verified = await this.verifyChangesetProof(syncChangeSet);
687
+ if (verified) {
688
+ await this._logging?.log({
689
+ level: "info",
690
+ source: this.CLASS_NAME,
691
+ message: "copyChangeSet",
692
+ data: {
693
+ changeSetStorageId: syncChangeSet.id
694
+ }
695
+ });
696
+ // Allocate a new id to the changeset copy and re-create a proof using this nodes identity
697
+ const copy = ObjectHelper.clone(syncChangeSet);
698
+ copy.id = Converter.bytesToHex(RandomHelper.generate(32));
699
+ copy.proof = await this.createChangeSetProof(copy);
700
+ // Store the copy
701
+ return {
702
+ syncChangeSet: copy,
703
+ changeSetStorageId: await this.storeChangeSet(copy)
704
+ };
705
+ }
706
+ }
707
+ }
367
708
  }
368
709
 
369
710
  // Copyright 2024 IOTA Stiftung.
@@ -385,7 +726,7 @@ class LocalSyncStateHelper {
385
726
  * The storage connector for the sync snapshot entries.
386
727
  * @internal
387
728
  */
388
- _localSyncSnapshotEntryEntityStorage;
729
+ _snapshotEntryEntityStorage;
389
730
  /**
390
731
  * The change set helper to use for applying changesets.
391
732
  * @internal
@@ -394,12 +735,12 @@ class LocalSyncStateHelper {
394
735
  /**
395
736
  * Create a new instance of LocalSyncStateHelper.
396
737
  * @param logging The logging connector to use for logging.
397
- * @param localSyncSnapshotEntryEntityStorage The storage connector for the local sync snapshot entries.
738
+ * @param snapshotEntryEntityStorage The storage connector for the sync snapshot entries.
398
739
  * @param changeSetHelper The change set helper to use for applying changesets.
399
740
  */
400
- constructor(logging, localSyncSnapshotEntryEntityStorage, changeSetHelper) {
741
+ constructor(logging, snapshotEntryEntityStorage, changeSetHelper) {
401
742
  this._logging = logging;
402
- this._localSyncSnapshotEntryEntityStorage = localSyncSnapshotEntryEntityStorage;
743
+ this._snapshotEntryEntityStorage = snapshotEntryEntityStorage;
403
744
  this._changeSetHelper = changeSetHelper;
404
745
  }
405
746
  /**
@@ -436,7 +777,7 @@ class LocalSyncStateHelper {
436
777
  await this.setLocalChangeSnapshot(localChangeSnapshot);
437
778
  }
438
779
  /**
439
- * Get the current local snapshot.
780
+ * Get the current local snapshot which contains just the changes for this node.
440
781
  * @param storageKey The storage key of the snapshot to get.
441
782
  * @returns The local snapshot entry.
442
783
  */
@@ -449,7 +790,7 @@ class LocalSyncStateHelper {
449
790
  storageKey
450
791
  }
451
792
  });
452
- const queryResult = await this._localSyncSnapshotEntryEntityStorage.query({
793
+ const queryResult = await this._snapshotEntryEntityStorage.query({
453
794
  conditions: [
454
795
  {
455
796
  property: "isLocalSnapshot",
@@ -491,7 +832,7 @@ class LocalSyncStateHelper {
491
832
  };
492
833
  }
493
834
  /**
494
- * Set the current local snapshot.
835
+ * Set the current local snapshot with changes for this node.
495
836
  * @param localChangeSnapshot The local change snapshot to set.
496
837
  * @returns Nothing.
497
838
  */
@@ -504,10 +845,10 @@ class LocalSyncStateHelper {
504
845
  storageKey: localChangeSnapshot.storageKey
505
846
  }
506
847
  });
507
- await this._localSyncSnapshotEntryEntityStorage.set(localChangeSnapshot);
848
+ await this._snapshotEntryEntityStorage.set(localChangeSnapshot);
508
849
  }
509
850
  /**
510
- * Get the current local snapshot.
851
+ * Get the current local snapshot with the changes for this node.
511
852
  * @param localChangeSnapshot The local change snapshot to remove.
512
853
  * @returns Nothing.
513
854
  */
@@ -520,47 +861,47 @@ class LocalSyncStateHelper {
520
861
  snapshotId: localChangeSnapshot.id
521
862
  }
522
863
  });
523
- await this._localSyncSnapshotEntryEntityStorage.remove(localChangeSnapshot.id);
864
+ await this._snapshotEntryEntityStorage.remove(localChangeSnapshot.id);
524
865
  }
525
866
  /**
526
- * Sync local data using a remote sync state.
867
+ * Apply a sync state to the local node.
527
868
  * @param storageKey The storage key of the snapshot to sync with.
528
- * @param remoteSyncState The sync state to sync with.
869
+ * @param syncState The sync state to sync with.
529
870
  * @returns Nothing.
530
871
  */
531
- async syncFromRemote(storageKey, remoteSyncState) {
872
+ async applySyncState(storageKey, syncState) {
532
873
  await this._logging?.log({
533
874
  level: "info",
534
875
  source: this.CLASS_NAME,
535
- message: "remoteSyncSynchronisation",
876
+ message: "applySyncState",
536
877
  data: {
537
- snapshotCount: remoteSyncState.snapshots.length
878
+ snapshotCount: syncState.snapshots.length
538
879
  }
539
880
  });
540
881
  // Sort from newest to oldest
541
- const sortedRemoteSnapshots = remoteSyncState.snapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
882
+ const sortedSnapshots = syncState.snapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
542
883
  const newSnapshots = [];
543
884
  const modifiedSnapshots = [];
544
- for (const remoteSnapshot of sortedRemoteSnapshots) {
885
+ for (const snapshot of sortedSnapshots) {
545
886
  await this._logging?.log({
546
887
  level: "info",
547
888
  source: this.CLASS_NAME,
548
- message: "remoteSyncSnapshotProcessing",
889
+ message: "applySnapshot",
549
890
  data: {
550
- snapshotId: remoteSnapshot.id,
551
- dateCreated: new Date(remoteSnapshot.dateCreated).toISOString()
891
+ snapshotId: snapshot.id,
892
+ dateCreated: new Date(snapshot.dateCreated).toISOString()
552
893
  }
553
894
  });
554
- const localSnapshot = await this._localSyncSnapshotEntryEntityStorage.get(remoteSnapshot.id);
895
+ const localSnapshot = await this._snapshotEntryEntityStorage.get(snapshot.id);
555
896
  const remoteSnapshotWithContext = {
556
- ...remoteSnapshot,
897
+ ...snapshot,
557
898
  storageKey
558
899
  };
559
900
  if (Is.empty(localSnapshot)) {
560
901
  // We don't have the snapshot locally, so we need to process it
561
902
  newSnapshots.push(remoteSnapshotWithContext);
562
903
  }
563
- else if (localSnapshot.dateModified !== remoteSnapshot.dateModified) {
904
+ else if (localSnapshot.dateModified !== snapshot.dateModified) {
564
905
  // If the local snapshot has a different dateModified, we need to update it
565
906
  modifiedSnapshots.push({
566
907
  localSnapshot,
@@ -582,13 +923,14 @@ class LocalSyncStateHelper {
582
923
  * Process the modified snapshots and store them in the local storage.
583
924
  * @param modifiedSnapshots The modified snapshots to process.
584
925
  * @returns Nothing.
926
+ * @internal
585
927
  */
586
928
  async processModifiedSnapshots(modifiedSnapshots) {
587
929
  for (const modifiedSnapshot of modifiedSnapshots) {
588
930
  await this._logging?.log({
589
931
  level: "info",
590
932
  source: this.CLASS_NAME,
591
- message: "remoteSyncSnapshotModified",
933
+ message: "processModifiedSnapshot",
592
934
  data: {
593
935
  snapshotId: modifiedSnapshot.remoteSnapshot.id,
594
936
  localModified: new Date(modifiedSnapshot.localSnapshot.dateModified ??
@@ -607,20 +949,21 @@ class LocalSyncStateHelper {
607
949
  }
608
950
  }
609
951
  }
610
- await this._localSyncSnapshotEntryEntityStorage.set(modifiedSnapshot.remoteSnapshot);
952
+ await this._snapshotEntryEntityStorage.set(modifiedSnapshot.remoteSnapshot);
611
953
  }
612
954
  }
613
955
  /**
614
956
  * Process the new snapshots and store them in the local storage.
615
957
  * @param newSnapshots The new snapshots to process.
616
958
  * @returns Nothing.
959
+ * @internal
617
960
  */
618
961
  async processNewSnapshots(newSnapshots) {
619
962
  for (const newSnapshot of newSnapshots) {
620
963
  await this._logging?.log({
621
964
  level: "info",
622
965
  source: this.CLASS_NAME,
623
- message: "remoteSyncSnapshotNew",
966
+ message: "processNewSnapshot",
624
967
  data: {
625
968
  snapshotId: newSnapshot.id,
626
969
  localModified: new Date(newSnapshot.dateCreated).toISOString()
@@ -632,11 +975,17 @@ class LocalSyncStateHelper {
632
975
  await this._changeSetHelper.getAndApplyChangeset(storageId);
633
976
  }
634
977
  }
635
- await this._localSyncSnapshotEntryEntityStorage.set(newSnapshot);
978
+ await this._snapshotEntryEntityStorage.set(newSnapshot);
636
979
  }
637
980
  }
638
981
  }
639
982
 
983
+ // Copyright 2024 IOTA Stiftung.
984
+ // SPDX-License-Identifier: Apache-2.0.
985
+ const SYNC_STATE_VERSION = "1";
986
+ const SYNC_POINTER_STORE_VERSION = "1";
987
+ const SYNC_SNAPSHOT_VERSION = "1";
988
+
640
989
  // Copyright 2024 IOTA Stiftung.
641
990
  // SPDX-License-Identifier: Apache-2.0.
642
991
  /**
@@ -658,10 +1007,10 @@ class RemoteSyncStateHelper {
658
1007
  */
659
1008
  _eventBusComponent;
660
1009
  /**
661
- * The blob storage component to use for remote sync states.
1010
+ * The blob storage helper.
662
1011
  * @internal
663
1012
  */
664
- _blobStorageComponent;
1013
+ _blobStorageHelper;
665
1014
  /**
666
1015
  * The verifiable storage connector to use for storing sync pointers.
667
1016
  * @internal
@@ -692,22 +1041,27 @@ class RemoteSyncStateHelper {
692
1041
  * @internal
693
1042
  */
694
1043
  _nodeIdentity;
1044
+ /**
1045
+ * Whether the node is trusted or not.
1046
+ * @internal
1047
+ */
1048
+ _isTrustedNode;
695
1049
  /**
696
1050
  * Create a new instance of DecentralisedEntityStorageConnector.
697
1051
  * @param logging The logging connector to use for logging.
698
1052
  * @param eventBusComponent The event bus component to use for events.
699
- * @param blobStorageComponent The blob storage component to use for remote sync states.
700
1053
  * @param verifiableSyncPointerStorageConnector The verifiable storage connector to use for storing sync pointers.
1054
+ * @param blobStorageHelper The blob storage helper to use for remote sync states.
701
1055
  * @param changeSetHelper The change set helper to use for managing changesets.
702
- * @param synchronisedStorageKey The synchronised storage key to use for verified storage operations.
1056
+ * @param isTrustedNode Whether the node is trusted or not.
703
1057
  */
704
- constructor(logging, eventBusComponent, blobStorageComponent, verifiableSyncPointerStorageConnector, changeSetHelper, synchronisedStorageKey) {
1058
+ constructor(logging, eventBusComponent, verifiableSyncPointerStorageConnector, blobStorageHelper, changeSetHelper, isTrustedNode) {
705
1059
  this._logging = logging;
706
1060
  this._eventBusComponent = eventBusComponent;
707
- this._blobStorageComponent = blobStorageComponent;
708
1061
  this._verifiableSyncPointerStorageConnector = verifiableSyncPointerStorageConnector;
709
1062
  this._changeSetHelper = changeSetHelper;
710
- this._synchronisedStorageKey = synchronisedStorageKey;
1063
+ this._blobStorageHelper = blobStorageHelper;
1064
+ this._isTrustedNode = isTrustedNode;
711
1065
  this._batchResponseStorageIds = {};
712
1066
  this._populateFullChanges = {};
713
1067
  this._eventBusComponent.subscribe(SynchronisedStorageTopics.BatchResponse, async (response) => {
@@ -725,17 +1079,24 @@ class RemoteSyncStateHelper {
725
1079
  this._nodeIdentity = nodeIdentity;
726
1080
  }
727
1081
  /**
728
- * Create and store a change set.
1082
+ * Set the synchronised storage key.
1083
+ * @param synchronisedStorageKey The synchronised storage key to use.
1084
+ */
1085
+ setSynchronisedStorageKey(synchronisedStorageKey) {
1086
+ this._synchronisedStorageKey = synchronisedStorageKey;
1087
+ }
1088
+ /**
1089
+ * Build a changeset.
729
1090
  * @param storageKey The storage key of the change set.
730
1091
  * @param changes The changes to apply.
731
1092
  * @param completeCallback The callback to call when the changeset is created and stored.
732
1093
  * @returns The storage id of the change set if created.
733
1094
  */
734
- async createAndStoreChangeSet(storageKey, changes, completeCallback) {
1095
+ async buildChangeSet(storageKey, changes, completeCallback) {
735
1096
  await this._logging?.log({
736
1097
  level: "info",
737
1098
  source: this.CLASS_NAME,
738
- message: "createAndStoreChangeSet",
1099
+ message: "buildingChangeSet",
739
1100
  data: {
740
1101
  storageKey,
741
1102
  changeCount: changes.length
@@ -794,12 +1155,14 @@ class RemoteSyncStateHelper {
794
1155
  for (const change of changes) {
795
1156
  change.entity = this._populateFullChanges[storageKey].entities[change.id] ?? change.entity;
796
1157
  if (change.operation === SyncChangeOperation.Set && Is.objectValue(change.entity)) {
1158
+ // Remove the id from the entity as this is stored in the operation
1159
+ // and will be reinstated when the changeset is reconstituted
1160
+ ObjectHelper.propertyDelete(change.entity, "id");
797
1161
  // Remove the node identity as the changeset has this stored at the top level
798
1162
  // and we do not want to store it in the change itself to reduce redundancy
799
1163
  ObjectHelper.propertyDelete(change.entity, "nodeIdentity");
800
1164
  }
801
1165
  }
802
- // Add the changeset to the current snapshot
803
1166
  const syncChangeSet = {
804
1167
  id: Converter.bytesToHex(RandomHelper.generate(32)),
805
1168
  dateCreated: new Date(Date.now()).toISOString(),
@@ -810,9 +1173,12 @@ class RemoteSyncStateHelper {
810
1173
  try {
811
1174
  // And sign it with the node identity
812
1175
  syncChangeSet.proof = await this._changeSetHelper.createChangeSetProof(syncChangeSet);
813
- // Store the changeset in the blob storage
814
- const changeSetStorageId = await this._changeSetHelper.storeChangeSet(syncChangeSet, this._nodeIdentity);
815
- await completeCallback(changeSetStorageId);
1176
+ // If this is a trusted node, we also store the changeset
1177
+ let changeSetStorageId;
1178
+ if (this._isTrustedNode) {
1179
+ changeSetStorageId = await this._changeSetHelper.storeChangeSet(syncChangeSet);
1180
+ }
1181
+ await completeCallback(syncChangeSet, changeSetStorageId);
816
1182
  }
817
1183
  catch (err) {
818
1184
  await this._logging?.log({
@@ -855,23 +1221,26 @@ class RemoteSyncStateHelper {
855
1221
  }
856
1222
  // No current sync state, so we create a new one
857
1223
  if (Is.empty(syncState)) {
858
- syncState = { snapshots: [] };
1224
+ syncState = { version: SYNC_STATE_VERSION, snapshots: [] };
859
1225
  }
860
1226
  // Sort the snapshots so the newest snapshot is last in the array
861
1227
  const sortedSnapshots = syncState.snapshots.sort((a, b) => a.dateCreated.localeCompare(b.dateCreated));
862
1228
  // Get the current snapshot, if it does not exist we create a new one
863
1229
  let currentSnapshot = sortedSnapshots[sortedSnapshots.length - 1];
1230
+ const now = new Date(Date.now()).toISOString();
864
1231
  if (Is.empty(currentSnapshot)) {
865
1232
  currentSnapshot = {
1233
+ version: SYNC_SNAPSHOT_VERSION,
866
1234
  id: Converter.bytesToHex(RandomHelper.generate(32)),
867
- dateCreated: new Date(Date.now()).toISOString(),
1235
+ dateCreated: now,
1236
+ dateModified: now,
868
1237
  changeSetStorageIds: []
869
1238
  };
870
1239
  syncState.snapshots.push(currentSnapshot);
871
1240
  }
872
1241
  else {
873
1242
  // Snapshot exists, we update the dateModified
874
- currentSnapshot.dateModified = new Date(Date.now()).toISOString();
1243
+ currentSnapshot.dateModified = now;
875
1244
  }
876
1245
  // Add the changeset storage id to the current snapshot
877
1246
  currentSnapshot.changeSetStorageIds.push(changeSetStorageId);
@@ -886,12 +1255,13 @@ class RemoteSyncStateHelper {
886
1255
  * @param batchSize The batch size to use for consolidation.
887
1256
  * @returns Nothing.
888
1257
  */
889
- async consolidateFromLocal(storageKey, batchSize) {
1258
+ async consolidationStart(storageKey, batchSize) {
890
1259
  await this._logging?.log({
891
1260
  level: "info",
892
1261
  source: this.CLASS_NAME,
893
1262
  message: "consolidationStarting"
894
1263
  });
1264
+ // Perform a batch request to start the consolidation
895
1265
  await this._eventBusComponent.publish(SynchronisedStorageTopics.BatchRequest, { storageKey, batchSize });
896
1266
  }
897
1267
  /**
@@ -899,44 +1269,47 @@ class RemoteSyncStateHelper {
899
1269
  * @returns The sync pointer store.
900
1270
  */
901
1271
  async getVerifiableSyncPointerStore() {
902
- try {
903
- await this._logging?.log({
904
- level: "info",
905
- source: this.CLASS_NAME,
906
- message: "verifiableSyncPointerStoreRetrieving",
907
- data: {
908
- key: this._synchronisedStorageKey
909
- }
910
- });
911
- const syncPointerStore = await this._verifiableSyncPointerStorageConnector.get(this._synchronisedStorageKey, { includeData: true });
912
- if (Is.uint8Array(syncPointerStore.data)) {
913
- const syncPointer = ObjectHelper.fromBytes(syncPointerStore.data);
1272
+ if (Is.stringValue(this._synchronisedStorageKey)) {
1273
+ try {
914
1274
  await this._logging?.log({
915
1275
  level: "info",
916
1276
  source: this.CLASS_NAME,
917
- message: "verifiableSyncPointerStoreRetrieved",
1277
+ message: "verifiableSyncPointerStoreRetrieving",
918
1278
  data: {
919
1279
  key: this._synchronisedStorageKey
920
1280
  }
921
1281
  });
922
- return syncPointer;
1282
+ const syncPointerStore = await this._verifiableSyncPointerStorageConnector.get(this._synchronisedStorageKey, { includeData: true });
1283
+ if (Is.uint8Array(syncPointerStore.data)) {
1284
+ const syncPointer = ObjectHelper.fromBytes(syncPointerStore.data);
1285
+ await this._logging?.log({
1286
+ level: "info",
1287
+ source: this.CLASS_NAME,
1288
+ message: "verifiableSyncPointerStoreRetrieved",
1289
+ data: {
1290
+ key: this._synchronisedStorageKey
1291
+ }
1292
+ });
1293
+ return syncPointer;
1294
+ }
923
1295
  }
924
- }
925
- catch (err) {
926
- if (!BaseError.someErrorName(err, NotFoundError.CLASS_NAME)) {
927
- throw err;
1296
+ catch (err) {
1297
+ if (!BaseError.someErrorName(err, NotFoundError.CLASS_NAME)) {
1298
+ throw err;
1299
+ }
928
1300
  }
1301
+ await this._logging?.log({
1302
+ level: "info",
1303
+ source: this.CLASS_NAME,
1304
+ message: "verifiableSyncPointerStoreNotFound",
1305
+ data: {
1306
+ key: this._synchronisedStorageKey
1307
+ }
1308
+ });
929
1309
  }
930
- await this._logging?.log({
931
- level: "info",
932
- source: this.CLASS_NAME,
933
- message: "verifiableSyncPointerStoreNotFound",
934
- data: {
935
- key: this._synchronisedStorageKey
936
- }
937
- });
938
1310
  // If no sync pointer store exists, we return an empty one
939
1311
  return {
1312
+ version: SYNC_POINTER_STORE_VERSION,
940
1313
  syncPointers: {}
941
1314
  };
942
1315
  }
@@ -946,7 +1319,7 @@ class RemoteSyncStateHelper {
946
1319
  * @returns Nothing.
947
1320
  */
948
1321
  async storeVerifiableSyncPointerStore(syncPointerStore) {
949
- if (this._nodeIdentity) {
1322
+ if (Is.stringValue(this._nodeIdentity) && Is.stringValue(this._synchronisedStorageKey)) {
950
1323
  await this._logging?.log({
951
1324
  level: "info",
952
1325
  source: this.CLASS_NAME,
@@ -973,9 +1346,7 @@ class RemoteSyncStateHelper {
973
1346
  snapshotCount: syncState.snapshots.length
974
1347
  }
975
1348
  });
976
- // We don't want to encrypt the sync state as no other nodes would be able to read it
977
- // the blob storage also needs to be publicly accessible so that other nodes can retrieve it
978
- return this._blobStorageComponent.create(Converter.bytesToBase64(ObjectHelper.toBytes(syncState)), undefined, undefined, undefined, { disableEncryption: true, compress: BlobStorageCompressionType.Gzip }, undefined, this._nodeIdentity);
1349
+ return this._blobStorageHelper.saveBlob(syncState);
979
1350
  }
980
1351
  /**
981
1352
  * Get the remote sync state.
@@ -992,11 +1363,8 @@ class RemoteSyncStateHelper {
992
1363
  syncPointerId
993
1364
  }
994
1365
  });
995
- const blobEntry = await this._blobStorageComponent.get(syncPointerId, {
996
- includeContent: true
997
- }, undefined, this._nodeIdentity);
998
- if (Is.stringBase64(blobEntry.blob)) {
999
- const syncState = ObjectHelper.fromBytes(Converter.base64ToBytes(blobEntry.blob));
1366
+ const syncState = await this._blobStorageHelper.load(syncPointerId);
1367
+ if (Is.object(syncState)) {
1000
1368
  await this._logging?.log({
1001
1369
  level: "info",
1002
1370
  source: this.CLASS_NAME,
@@ -1009,10 +1377,16 @@ class RemoteSyncStateHelper {
1009
1377
  return syncState;
1010
1378
  }
1011
1379
  }
1012
- catch (err) {
1013
- if (!BaseError.someErrorName(err, NotFoundError.CLASS_NAME)) {
1014
- throw err;
1015
- }
1380
+ catch (error) {
1381
+ await this._logging?.log({
1382
+ level: "warn",
1383
+ source: this.CLASS_NAME,
1384
+ message: "getSyncStateError",
1385
+ data: {
1386
+ syncPointerId
1387
+ },
1388
+ error: BaseError.fromError(error)
1389
+ });
1016
1390
  }
1017
1391
  await this._logging?.log({
1018
1392
  level: "info",
@@ -1024,18 +1398,20 @@ class RemoteSyncStateHelper {
1024
1398
  });
1025
1399
  }
1026
1400
  /**
1027
- * Handle the batch response.
1401
+ * Handle the batch response which is triggered from a consolidation request.
1028
1402
  * @param response The batch response to handle.
1029
1403
  */
1030
1404
  async handleBatchResponse(response) {
1031
1405
  if (Is.stringValue(this._nodeIdentity)) {
1406
+ const now = new Date(Date.now()).toISOString();
1032
1407
  // Create a new snapshot entry for the current batch
1033
1408
  const syncChangeSet = {
1034
1409
  id: Converter.bytesToHex(RandomHelper.generate(32)),
1035
- dateCreated: new Date(Date.now()).toISOString(),
1410
+ dateCreated: now,
1411
+ dateModified: now,
1036
1412
  changes: response.entities.map(change => ({
1037
1413
  operation: SyncChangeOperation.Set,
1038
- id: change[response.primaryKey]
1414
+ id: change.id
1039
1415
  })),
1040
1416
  storageKey: response.storageKey,
1041
1417
  nodeIdentity: this._nodeIdentity
@@ -1043,25 +1419,37 @@ class RemoteSyncStateHelper {
1043
1419
  // And sign it with the node identity
1044
1420
  syncChangeSet.proof = await this._changeSetHelper.createChangeSetProof(syncChangeSet);
1045
1421
  // Store the changeset in the blob storage
1046
- const changeSetStorageId = await this._changeSetHelper.storeChangeSet(syncChangeSet, this._nodeIdentity);
1422
+ const changeSetStorageId = await this._changeSetHelper.storeChangeSet(syncChangeSet);
1047
1423
  // Add the changeset storage id to the snapshot ids
1048
1424
  this._batchResponseStorageIds[response.storageKey] ??= [];
1049
1425
  this._batchResponseStorageIds[response.storageKey].push(changeSetStorageId);
1426
+ // If this is the last entry in the batch response, we can create the consolidated snapshot
1050
1427
  if (response.lastEntry) {
1051
- const syncState = { snapshots: [] };
1428
+ // Get the current sync pointer store
1429
+ const syncPointerStore = await this.getVerifiableSyncPointerStore();
1430
+ let syncState;
1431
+ if (Is.stringValue(syncPointerStore.syncPointers[response.storageKey])) {
1432
+ // If the sync pointer exists, we load the current sync state
1433
+ syncState = await this.getRemoteSyncState(syncPointerStore.syncPointers[response.storageKey]);
1434
+ }
1435
+ // If the sync state does not exist, we create a new one
1436
+ syncState ??= { version: SYNC_STATE_VERSION, snapshots: [] };
1052
1437
  const batchSnapshot = {
1438
+ version: SYNC_SNAPSHOT_VERSION,
1053
1439
  id: Converter.bytesToHex(RandomHelper.generate(32)),
1054
- dateCreated: new Date(Date.now()).toISOString(),
1440
+ dateCreated: now,
1441
+ dateModified: now,
1055
1442
  changeSetStorageIds: this._batchResponseStorageIds[response.storageKey]
1056
1443
  };
1057
1444
  syncState.snapshots.push(batchSnapshot);
1058
1445
  // Store the sync state in the blob storage
1059
1446
  const syncStateId = await this.storeRemoteSyncState(syncState);
1060
- // Get the current sync pointer store
1061
- const syncPointerStore = await this.getVerifiableSyncPointerStore();
1062
1447
  syncPointerStore.syncPointers[response.storageKey] = syncStateId;
1063
1448
  // Store the verifiable sync pointer in the verifiable storage
1064
1449
  await this.storeVerifiableSyncPointerStore(syncPointerStore);
1450
+ // Remove the batch response storage ids for the storage key
1451
+ // as we have consolidated the changes
1452
+ delete this._batchResponseStorageIds[response.storageKey];
1065
1453
  await this._logging?.log({
1066
1454
  level: "info",
1067
1455
  source: this.CLASS_NAME,
@@ -1130,16 +1518,21 @@ class SynchronisedStorageService {
1130
1518
  * @internal
1131
1519
  */
1132
1520
  _eventBusComponent;
1521
+ /**
1522
+ * The vault connector.
1523
+ * @internal
1524
+ */
1525
+ _vaultConnector;
1133
1526
  /**
1134
1527
  * The storage connector for the sync snapshot entries.
1135
1528
  * @internal
1136
1529
  */
1137
1530
  _localSyncSnapshotEntryEntityStorage;
1138
1531
  /**
1139
- * The blob storage component to use for remote sync states.
1532
+ * The blob storage connector to use for remote sync states.
1140
1533
  * @internal
1141
1534
  */
1142
- _blobStorageComponent;
1535
+ _blobStorageConnector;
1143
1536
  /**
1144
1537
  * The verifiable storage connector to use for storing sync pointers.
1145
1538
  * @internal
@@ -1160,6 +1553,11 @@ class SynchronisedStorageService {
1160
1553
  * @internal
1161
1554
  */
1162
1555
  _trustedSynchronisedStorageComponent;
1556
+ /**
1557
+ * The blob storage helper.
1558
+ * @internal
1559
+ */
1560
+ _blobStorageHelper;
1163
1561
  /**
1164
1562
  * The change set helper.
1165
1563
  * @internal
@@ -1180,6 +1578,11 @@ class SynchronisedStorageService {
1180
1578
  * @internal
1181
1579
  */
1182
1580
  _config;
1581
+ /**
1582
+ * The synchronised storage key to use for the remote synchronised storage.
1583
+ * @internal
1584
+ */
1585
+ _synchronisedStorageKey;
1183
1586
  /**
1184
1587
  * The flag to determine if the service has been started.
1185
1588
  * @internal
@@ -1204,13 +1607,13 @@ class SynchronisedStorageService {
1204
1607
  Guards.object(this.CLASS_NAME, "options.config", options.config);
1205
1608
  this._eventBusComponent = ComponentFactory.get(options.eventBusComponentType ?? "event-bus");
1206
1609
  this._logging = LoggingConnectorFactory.getIfExists(options.loggingConnectorType ?? "logging");
1610
+ this._vaultConnector = VaultConnectorFactory.get(options.vaultConnectorType ?? "vault");
1207
1611
  this._localSyncSnapshotEntryEntityStorage = EntityStorageConnectorFactory.get(options.syncSnapshotStorageConnectorType ?? "sync-snapshot-entry");
1208
1612
  this._verifiableSyncPointerStorageConnector = VerifiableStorageConnectorFactory.get(options.verifiableStorageConnectorType ?? "verifiable-storage");
1209
- this._blobStorageComponent = ComponentFactory.get(options.blobStorageComponentType ?? "blob-storage");
1613
+ this._blobStorageConnector = BlobStorageConnectorFactory.get(options.blobStorageConnectorType ?? "blob-storage");
1210
1614
  this._identityConnector = IdentityConnectorFactory.get(options.identityConnectorType ?? "identity");
1211
1615
  this._taskSchedulerComponent = ComponentFactory.get(options.taskSchedulerComponentType ?? "task-scheduler");
1212
1616
  this._config = {
1213
- synchronisedStorageKey: options.config.synchronisedStorageKey,
1214
1617
  synchronisedStorageMethodId: options.config.synchronisedStorageMethodId ?? "synchronised-storage-assertion",
1215
1618
  entityUpdateIntervalMinutes: options.config.entityUpdateIntervalMinutes ??
1216
1619
  SynchronisedStorageService._DEFAULT_ENTITY_UPDATE_INTERVAL_MINUTES,
@@ -1218,8 +1621,12 @@ class SynchronisedStorageService {
1218
1621
  consolidationIntervalMinutes: options.config.consolidationIntervalMinutes ??
1219
1622
  SynchronisedStorageService._DEFAULT_CONSOLIDATION_INTERVAL_MINUTES,
1220
1623
  consolidationBatchSize: options.config.consolidationBatchSize ??
1221
- SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE
1624
+ SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE,
1625
+ blobStorageEncryptionKeyId: options.config.blobStorageEncryptionKeyId ?? "synchronised-storage-blob-encryption-key",
1626
+ verifiableStorageKeyId: options.config.verifiableStorageKeyId
1222
1627
  };
1628
+ this._synchronisedStorageKey =
1629
+ verifiableStorageKeys[options.config.verifiableStorageKeyId] ?? options.config.verifiableStorageKeyId;
1223
1630
  // If this is not a trusted node, we need to use a synchronised storage service
1224
1631
  // to synchronise with a trusted node.
1225
1632
  if (!this._config.isTrustedNode) {
@@ -1227,12 +1634,13 @@ class SynchronisedStorageService {
1227
1634
  this._trustedSynchronisedStorageComponent =
1228
1635
  ComponentFactory.get(options.trustedSynchronisedStorageComponentType);
1229
1636
  }
1230
- this._changeSetHelper = new ChangeSetHelper(this._logging, this._eventBusComponent, this._blobStorageComponent, this._identityConnector, this._config.synchronisedStorageMethodId);
1637
+ this._blobStorageHelper = new BlobStorageHelper(this._logging, this._vaultConnector, this._blobStorageConnector, this._config.blobStorageEncryptionKeyId, this._config.isTrustedNode);
1638
+ this._changeSetHelper = new ChangeSetHelper(this._logging, this._eventBusComponent, this._identityConnector, this._blobStorageHelper, this._config.synchronisedStorageMethodId);
1231
1639
  this._localSyncStateHelper = new LocalSyncStateHelper(this._logging, this._localSyncSnapshotEntryEntityStorage, this._changeSetHelper);
1232
- this._remoteSyncStateHelper = new RemoteSyncStateHelper(this._logging, this._eventBusComponent, this._blobStorageComponent, this._verifiableSyncPointerStorageConnector, this._changeSetHelper, this._config.synchronisedStorageKey);
1640
+ this._remoteSyncStateHelper = new RemoteSyncStateHelper(this._logging, this._eventBusComponent, this._verifiableSyncPointerStorageConnector, this._blobStorageHelper, this._changeSetHelper, this._config.isTrustedNode);
1233
1641
  this._serviceStarted = false;
1234
1642
  this._activeStorageKeys = {};
1235
- this._eventBusComponent.subscribe(SynchronisedStorageTopics.RegisterStorageKey, async (event) => this.registerType(event.data));
1643
+ this._eventBusComponent.subscribe(SynchronisedStorageTopics.RegisterStorageKey, async (event) => this.registerStorageKey(event.data));
1236
1644
  this._eventBusComponent.subscribe(SynchronisedStorageTopics.LocalItemChange, async (event) => this._localSyncStateHelper.addLocalChange(event.data.storageKey, event.data.operation, event.data.id));
1237
1645
  }
1238
1646
  /**
@@ -1245,7 +1653,16 @@ class SynchronisedStorageService {
1245
1653
  async start(nodeIdentity, nodeLoggingConnectorType, componentState) {
1246
1654
  this._nodeIdentity = nodeIdentity;
1247
1655
  this._remoteSyncStateHelper.setNodeIdentity(nodeIdentity);
1656
+ this._changeSetHelper.setNodeIdentity(nodeIdentity);
1657
+ this._remoteSyncStateHelper.setSynchronisedStorageKey(this._synchronisedStorageKey);
1248
1658
  this._serviceStarted = true;
1659
+ // If this is not a trusted node we need to request the decryption key from a trusted node
1660
+ if (!this._config.isTrustedNode && !Is.empty(this._trustedSynchronisedStorageComponent)) {
1661
+ const proof = await this._identityConnector.createProof(this._nodeIdentity, DocumentHelper.joinId(this._nodeIdentity, this._config.synchronisedStorageMethodId), ProofTypes.DataIntegrityProof, { nodeIdentity });
1662
+ const decryptionKey = await this._trustedSynchronisedStorageComponent.getDecryptionKey(this._nodeIdentity, proof);
1663
+ // We don't have the private key so instead we store the key as a secret in the vault
1664
+ await this._vaultConnector.setSecret(this._config.blobStorageEncryptionKeyId, decryptionKey);
1665
+ }
1249
1666
  // If there are already storage keys registered, we need to activate them
1250
1667
  for (const storageKey in this._activeStorageKeys) {
1251
1668
  await this.activateStorageKey(storageKey);
@@ -1266,24 +1683,59 @@ class SynchronisedStorageService {
1266
1683
  }
1267
1684
  }
1268
1685
  /**
1269
- * Synchronise a complete set of changes, assumes this is a trusted node.
1270
- * @param changeSetStorageId The id of the change set to synchronise in blob storage.
1686
+ * Get the decryption key for the synchronised storage.
1687
+ * This is used to decrypt the data stored in the synchronised storage.
1688
+ * @param nodeIdentity The identity of the node requesting the decryption key.
1689
+ * @param proof The proof of the request so we know the request is from the specified node.
1690
+ * @returns The decryption key.
1691
+ */
1692
+ async getDecryptionKey(nodeIdentity, proof) {
1693
+ if (!this._config.isTrustedNode) {
1694
+ throw new GeneralError(this.CLASS_NAME, "notTrustedNode");
1695
+ }
1696
+ Guards.stringValue(this.CLASS_NAME, "nodeIdentity", nodeIdentity);
1697
+ Guards.object(this.CLASS_NAME, "proof", proof);
1698
+ const isValid = await this._identityConnector.verifyProof({ nodeIdentity }, proof);
1699
+ if (!isValid) {
1700
+ throw new UnauthorizedError(this.CLASS_NAME, "invalidProof");
1701
+ }
1702
+ // TODO: We need to check if the node has permissions to access the decryption key
1703
+ // using rights-management
1704
+ const key = await this._vaultConnector.getKey(this._config.blobStorageEncryptionKeyId);
1705
+ if (Is.undefined(key.publicKey)) {
1706
+ throw new UnauthorizedError(this.CLASS_NAME, "decryptionKeyNotFound");
1707
+ }
1708
+ return Converter.bytesToBase64(key.publicKey);
1709
+ }
1710
+ /**
1711
+ * Synchronise a set of changes from an untrusted node, assumes this is a trusted node.
1712
+ * @param syncChangeSet The change set to synchronise.
1271
1713
  * @returns Nothing.
1272
1714
  */
1273
- async syncChangeSet(changeSetStorageId) {
1715
+ async syncChangeSet(syncChangeSet) {
1274
1716
  if (!this._config.isTrustedNode) {
1275
1717
  throw new GeneralError(this.CLASS_NAME, "notTrustedNode");
1276
1718
  }
1277
- // This method is called by non trusted nodes to synchronise changes
1278
- Guards.stringValue(this.CLASS_NAME, "changeSetStorageId", changeSetStorageId);
1719
+ Guards.object(this.CLASS_NAME, "syncChangeSet", syncChangeSet);
1720
+ await this._logging?.log({
1721
+ level: "info",
1722
+ source: this.CLASS_NAME,
1723
+ message: "syncChangeSetForRemoteNode",
1724
+ data: {
1725
+ changeSetStorageId: syncChangeSet.id
1726
+ }
1727
+ });
1279
1728
  // TODO: The change set has a proof signed by the originating node identity
1280
1729
  // The proof is verified that the change set is valid and has not been tampered with.
1281
1730
  // but we also need to check that the originating node has permissions
1282
1731
  // to store the change set in the synchronised storage.
1283
1732
  // This will be performed using rights-management
1284
- const changeSet = await this._changeSetHelper.getAndApplyChangeset(changeSetStorageId);
1285
- if (!Is.empty(changeSet)) {
1286
- await this._remoteSyncStateHelper.addChangeSetToSyncState(changeSet.storageKey, changeSetStorageId);
1733
+ const copy = await this._changeSetHelper.copyChangeset(syncChangeSet);
1734
+ if (!Is.empty(copy) && Is.stringValue(this._nodeIdentity)) {
1735
+ // Apply the changes to this node
1736
+ await this._changeSetHelper.applyChangeset(copy.syncChangeSet);
1737
+ // And update the sync state with the latest changes
1738
+ await this._remoteSyncStateHelper.addChangeSetToSyncState(copy.syncChangeSet.storageKey, copy.changeSetStorageId);
1287
1739
  }
1288
1740
  }
1289
1741
  /**
@@ -1339,7 +1791,7 @@ class SynchronisedStorageService {
1339
1791
  const remoteSyncState = await this._remoteSyncStateHelper.getRemoteSyncState(verifiableSyncPointerStore.syncPointers[storageKey]);
1340
1792
  // If we got the sync state we can try and sync from it
1341
1793
  if (!Is.undefined(remoteSyncState)) {
1342
- await this._localSyncStateHelper.syncFromRemote(storageKey, remoteSyncState);
1794
+ await this._localSyncStateHelper.applySyncState(storageKey, remoteSyncState);
1343
1795
  }
1344
1796
  }
1345
1797
  }
@@ -1359,38 +1811,51 @@ class SynchronisedStorageService {
1359
1811
  });
1360
1812
  const localChangeSnapshot = await this._localSyncStateHelper.getLocalChangeSnapshot(storageKey);
1361
1813
  if (Is.arrayValue(localChangeSnapshot.changes)) {
1362
- await this._remoteSyncStateHelper.createAndStoreChangeSet(storageKey, localChangeSnapshot.changes, async (changeSetStorageId) => {
1363
- if (Is.stringValue(changeSetStorageId)) {
1814
+ await this._remoteSyncStateHelper.buildChangeSet(storageKey, localChangeSnapshot.changes, async (syncChangeSet, changeSetStorageId) => {
1815
+ if (Is.empty(syncChangeSet) && Is.empty(changeSetStorageId)) {
1364
1816
  await this._logging?.log({
1365
1817
  level: "info",
1366
1818
  source: this.CLASS_NAME,
1367
- message: "createdStorageChangeSet",
1819
+ message: "builtStorageChangeSetNone",
1820
+ data: {
1821
+ storageKey
1822
+ }
1823
+ });
1824
+ }
1825
+ else {
1826
+ await this._logging?.log({
1827
+ level: "info",
1828
+ source: this.CLASS_NAME,
1829
+ message: "builtStorageChangeSet",
1368
1830
  data: {
1369
1831
  storageKey,
1370
1832
  changeSetStorageId
1371
1833
  }
1372
1834
  });
1373
1835
  // Send the local changes to the remote storage if we are a trusted node
1374
- if (this._config.isTrustedNode) {
1836
+ if (this._config.isTrustedNode && Is.stringValue(changeSetStorageId)) {
1837
+ // If we are a trusted node, we can add the change set to the sync state
1838
+ // and remove the local change snapshot
1375
1839
  await this._remoteSyncStateHelper.addChangeSetToSyncState(storageKey, changeSetStorageId);
1376
1840
  await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
1377
1841
  }
1378
- else if (!Is.empty(this._trustedSynchronisedStorageComponent)) {
1842
+ else if (!Is.empty(this._trustedSynchronisedStorageComponent) &&
1843
+ Is.object(syncChangeSet)) {
1379
1844
  // If we are not a trusted node, we need to send the changes to the trusted node
1380
- await this._trustedSynchronisedStorageComponent.syncChangeSet(changeSetStorageId);
1845
+ // and then remove the local change snapshot
1846
+ await this._logging?.log({
1847
+ level: "info",
1848
+ source: this.CLASS_NAME,
1849
+ message: "sendingChangeSetToTrustedNode",
1850
+ data: {
1851
+ storageKey,
1852
+ changeSetStorageId
1853
+ }
1854
+ });
1855
+ await this._trustedSynchronisedStorageComponent.syncChangeSet(syncChangeSet);
1381
1856
  await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
1382
1857
  }
1383
1858
  }
1384
- else {
1385
- await this._logging?.log({
1386
- level: "info",
1387
- source: this.CLASS_NAME,
1388
- message: "createdStorageChangeSetNone",
1389
- data: {
1390
- storageKey
1391
- }
1392
- });
1393
- }
1394
1859
  });
1395
1860
  }
1396
1861
  else {
@@ -1413,17 +1878,16 @@ class SynchronisedStorageService {
1413
1878
  async startConsolidationSync(storageKey) {
1414
1879
  let localChangeSnapshot;
1415
1880
  try {
1416
- // If we are performing a consolidation, we can remove the local changes
1417
- await this._localSyncStateHelper.getLocalChangeSnapshot(storageKey);
1881
+ // If we are performing a consolidation, we can remove the local change snapshot
1882
+ // as we are going to create a complete changeset from the DB
1883
+ localChangeSnapshot = await this._localSyncStateHelper.getLocalChangeSnapshot(storageKey);
1418
1884
  if (!Is.empty(localChangeSnapshot)) {
1419
1885
  await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
1420
1886
  }
1421
- if (Is.stringValue(this._nodeIdentity)) {
1422
- await this._remoteSyncStateHelper.consolidateFromLocal(storageKey, this._config.consolidationBatchSize ??
1423
- SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE);
1424
- // The consolidation was successful, so we can remove the local change snapshot permanently
1425
- localChangeSnapshot = undefined;
1426
- }
1887
+ await this._remoteSyncStateHelper.consolidationStart(storageKey, this._config.consolidationBatchSize ??
1888
+ SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE);
1889
+ // The consolidation was successful, so we can remove the local change snapshot permanently
1890
+ localChangeSnapshot = undefined;
1427
1891
  }
1428
1892
  catch (error) {
1429
1893
  if (localChangeSnapshot) {
@@ -1440,22 +1904,22 @@ class SynchronisedStorageService {
1440
1904
  }
1441
1905
  /**
1442
1906
  * Register a new sync type.
1443
- * @param syncRegisterType The sync register type to register.
1907
+ * @param syncRegisterStorageKey The sync register type to register.
1444
1908
  * @internal
1445
1909
  */
1446
- async registerType(syncRegisterType) {
1910
+ async registerStorageKey(syncRegisterStorageKey) {
1447
1911
  await this._logging?.log({
1448
1912
  level: "info",
1449
1913
  source: this.CLASS_NAME,
1450
- message: "registerType",
1914
+ message: "registerStorageKey",
1451
1915
  data: {
1452
- storageKey: syncRegisterType.storageKey
1916
+ storageKey: syncRegisterStorageKey.storageKey
1453
1917
  }
1454
1918
  });
1455
- if (Is.empty(this._activeStorageKeys[syncRegisterType.storageKey])) {
1456
- this._activeStorageKeys[syncRegisterType.storageKey] = false;
1919
+ if (Is.empty(this._activeStorageKeys[syncRegisterStorageKey.storageKey])) {
1920
+ this._activeStorageKeys[syncRegisterStorageKey.storageKey] = false;
1457
1921
  if (this._serviceStarted) {
1458
- await this.activateStorageKey(syncRegisterType.storageKey);
1922
+ await this.activateStorageKey(syncRegisterStorageKey.storageKey);
1459
1923
  }
1460
1924
  }
1461
1925
  }
@@ -1469,7 +1933,7 @@ class SynchronisedStorageService {
1469
1933
  await this._logging?.log({
1470
1934
  level: "info",
1471
1935
  source: this.CLASS_NAME,
1472
- message: "activateType",
1936
+ message: "activateStorageKey",
1473
1937
  data: {
1474
1938
  storageKey
1475
1939
  }
@@ -1495,4 +1959,4 @@ class SynchronisedStorageService {
1495
1959
  }
1496
1960
  }
1497
1961
 
1498
- export { SyncSnapshotEntry, SynchronisedStorageService, generateRestRoutesSynchronisedStorage, initSchema, restEntryPoints, synchronisedStorageSyncChangeSetRequest, tagsSynchronisedStorage };
1962
+ export { SyncSnapshotEntry, SynchronisedStorageService, generateRestRoutesSynchronisedStorage, initSchema, restEntryPoints, synchronisedStorageGetDecryptionKeyRequest, synchronisedStorageSyncChangeSetRequest, tagsSynchronisedStorage };