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