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

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 +963 -276
  2. package/dist/esm/index.mjs +965 -279
  3. package/dist/types/entities/syncSnapshotEntry.d.ts +12 -5
  4. package/dist/types/helpers/blobStorageHelper.d.ts +39 -0
  5. package/dist/types/helpers/changeSetHelper.d.ts +26 -7
  6. package/dist/types/helpers/localSyncStateHelper.d.ts +11 -25
  7. package/dist/types/helpers/remoteSyncStateHelper.d.ts +17 -12
  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 +9 -1
  12. package/dist/types/models/ISyncState.d.ts +8 -0
  13. package/dist/types/models/ISynchronisedStorageServiceConfig.d.ts +17 -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 +128 -0
  18. package/docs/changelog.md +29 -0
  19. package/docs/open-api/spec.json +246 -18
  20. package/docs/reference/classes/SyncSnapshotEntry.md +22 -6
  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 +18 -2
  26. package/docs/reference/interfaces/ISyncState.md +16 -0
  27. package/docs/reference/interfaces/ISynchronisedStorageServiceConfig.md +44 -10
  28. package/docs/reference/interfaces/ISynchronisedStorageServiceConstructorOptions.md +11 -3
  29. package/locales/en.json +60 -25
  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.
@@ -21,6 +23,10 @@ exports.SyncSnapshotEntry = class SyncSnapshotEntry {
21
23
  * The id for the snapshot.
22
24
  */
23
25
  id;
26
+ /**
27
+ * The version for the snapshot.
28
+ */
29
+ version;
24
30
  /**
25
31
  * The storage key for the snapshot i.e. which entity is being synchronized.
26
32
  */
@@ -34,9 +40,13 @@ exports.SyncSnapshotEntry = class SyncSnapshotEntry {
34
40
  */
35
41
  dateModified;
36
42
  /**
37
- * The flag to determine if this is the current local snapshot containing changes for this node.
43
+ * The flag to determine if this is the snapshot is the local one containing changes for this node.
44
+ */
45
+ isLocal;
46
+ /**
47
+ * The flag to determine if this is a consolidated snapshot.
38
48
  */
39
- isLocalSnapshot;
49
+ isConsolidated;
40
50
  /**
41
51
  * The ids of the storage for the change sets in the snapshot, if this is not a local snapshot.
42
52
  */
@@ -50,6 +60,10 @@ __decorate([
50
60
  entity.property({ type: "string", isPrimary: true }),
51
61
  __metadata("design:type", String)
52
62
  ], exports.SyncSnapshotEntry.prototype, "id", void 0);
63
+ __decorate([
64
+ entity.property({ type: "string" }),
65
+ __metadata("design:type", String)
66
+ ], exports.SyncSnapshotEntry.prototype, "version", void 0);
53
67
  __decorate([
54
68
  entity.property({ type: "string", isSecondary: true }),
55
69
  __metadata("design:type", String)
@@ -59,13 +73,17 @@ __decorate([
59
73
  __metadata("design:type", String)
60
74
  ], exports.SyncSnapshotEntry.prototype, "dateCreated", void 0);
61
75
  __decorate([
62
- entity.property({ type: "string", optional: true }),
76
+ entity.property({ type: "string" }),
63
77
  __metadata("design:type", String)
64
78
  ], exports.SyncSnapshotEntry.prototype, "dateModified", void 0);
65
79
  __decorate([
66
- entity.property({ type: "boolean", optional: true }),
80
+ entity.property({ type: "boolean" }),
81
+ __metadata("design:type", Boolean)
82
+ ], exports.SyncSnapshotEntry.prototype, "isLocal", void 0);
83
+ __decorate([
84
+ entity.property({ type: "boolean" }),
67
85
  __metadata("design:type", Boolean)
68
- ], exports.SyncSnapshotEntry.prototype, "isLocalSnapshot", void 0);
86
+ ], exports.SyncSnapshotEntry.prototype, "isConsolidated", void 0);
69
87
  __decorate([
70
88
  entity.property({ type: "array", itemType: "string", optional: true }),
71
89
  __metadata("design:type", Array)
@@ -102,8 +120,8 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
102
120
  operationId: "synchronisedStorageSyncChangeSetRequest",
103
121
  summary: "Request that the node perform a sync request for a changeset.",
104
122
  tag: tagsSynchronisedStorage[0].name,
105
- method: "GET",
106
- path: `${baseRouteName}/`,
123
+ method: "POST",
124
+ path: `${baseRouteName}/sync-changeset`,
107
125
  handler: async (httpRequestContext, request) => synchronisedStorageSyncChangeSetRequest(httpRequestContext, componentName, request),
108
126
  requestType: {
109
127
  type: "ISyncChangeSetRequest",
@@ -111,8 +129,30 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
111
129
  {
112
130
  id: "synchronisedStorageSyncChangeSetRequestExample",
113
131
  request: {
114
- query: {
115
- changeSetStorageId: "12345"
132
+ body: {
133
+ id: "0909090909090909090909090909090909090909090909090909090909090909",
134
+ dateCreated: "2025-05-29T01:00:00.000Z",
135
+ dateModified: "2025-05-29T01:00:00.000Z",
136
+ nodeIdentity: "did:entity-storage:0xd2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
137
+ changes: [
138
+ {
139
+ entity: {
140
+ dateModified: "2025-01-01T00:00:00.000Z"
141
+ },
142
+ id: "test-id-1",
143
+ operation: "set"
144
+ }
145
+ ],
146
+ proof: {
147
+ "@context": "https://www.w3.org/ns/credentials/v2",
148
+ created: "2025-05-29T01:00:00.000Z",
149
+ cryptosuite: "eddsa-jcs-2022",
150
+ proofPurpose: "assertionMethod",
151
+ proofValue: "z5efBErQs3YBLZoH7jgKMQaRc9YjAxA5XSYKmW3FmTBDw9WionT2NS2x1SMvcRyBvw53cSSoaCT1xQH9tkWngGCX3",
152
+ type: "DataIntegrityProof",
153
+ verificationMethod: "did:entity-storage:0xd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0#synchronised-storage-assertion"
154
+ },
155
+ storageKey: "test-type"
116
156
  }
117
157
  }
118
158
  }
@@ -122,9 +162,61 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
122
162
  {
123
163
  type: "INoContentResponse"
124
164
  }
125
- ]
165
+ ],
166
+ // Authentication is provided by the proof in the request body.
167
+ skipAuth: true
126
168
  };
127
- return [syncChangeSetRoute];
169
+ const getDecryptionKeyRoute = {
170
+ operationId: "synchronisedStorageGetDecryptionKeyRequest",
171
+ summary: "Request the decryption key.",
172
+ tag: tagsSynchronisedStorage[0].name,
173
+ method: "POST",
174
+ path: `${baseRouteName}/decryption-key`,
175
+ handler: async (httpRequestContext, request) => synchronisedStorageGetDecryptionKeyRequest(httpRequestContext, componentName, request),
176
+ requestType: {
177
+ type: "ISyncChangeSetRequest",
178
+ examples: [
179
+ {
180
+ id: "synchronisedStorageSyncGetDecryptionKeyRequestExample",
181
+ request: {
182
+ body: {
183
+ nodeIdentity: "did:entity-storage:0xd2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
184
+ proof: {
185
+ "@context": "https://www.w3.org/ns/credentials/v2",
186
+ created: "2025-05-29T01:00:00.000Z",
187
+ cryptosuite: "eddsa-jcs-2022",
188
+ proofPurpose: "assertionMethod",
189
+ proofValue: "z5efBErQs3YBLZoH7jgKMQaRc9YjAxA5XSYKmW3FmTBDw9WionT2NS2x1SMvcRyBvw53cSSoaCT1xQH9tkWngGCX3",
190
+ type: "DataIntegrityProof",
191
+ verificationMethod: "did:entity-storage:0xd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0#synchronised-storage-assertion"
192
+ }
193
+ }
194
+ }
195
+ }
196
+ ]
197
+ },
198
+ responseType: [
199
+ {
200
+ type: "ISyncDecryptionKeyResponse",
201
+ examples: [
202
+ {
203
+ id: "synchronisedStorageSyncGetDecryptionKeyResponseExample",
204
+ response: {
205
+ body: {
206
+ decryptionKey: "z5efBErQs3YBLZoH7jgKMQaRc9YjAxA5XSYKmW3FmTBDw9WionT2NS2x1SMvcRyBvw53cSSoaCT1xQH9tkWngGCX3"
207
+ }
208
+ }
209
+ }
210
+ ]
211
+ },
212
+ {
213
+ type: "IUnauthorizedResponse"
214
+ }
215
+ ],
216
+ // Authentication is provided by the proof in the request body.
217
+ skipAuth: true
218
+ };
219
+ return [syncChangeSetRoute, getDecryptionKeyRoute];
128
220
  }
129
221
  /**
130
222
  * Perform the sync change set operation.
@@ -135,13 +227,31 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
135
227
  */
136
228
  async function synchronisedStorageSyncChangeSetRequest(httpRequestContext, componentName, request) {
137
229
  core.Guards.object(ROUTES_SOURCE, "request", request);
138
- core.Guards.object(ROUTES_SOURCE, "request.query", request.query);
230
+ core.Guards.object(ROUTES_SOURCE, "request.body", request.body);
139
231
  const component = core.ComponentFactory.get(componentName);
140
- await component.syncChangeSet(request.query.changeSetStorageId);
232
+ await component.syncChangeSet(request.body);
141
233
  return {
142
234
  statusCode: web.HttpStatusCode.noContent
143
235
  };
144
236
  }
237
+ /**
238
+ * Request the decryption key.
239
+ * @param httpRequestContext The request context for the API.
240
+ * @param componentName The name of the component to use in the routes.
241
+ * @param request The request.
242
+ * @returns The response object with additional http response properties.
243
+ */
244
+ async function synchronisedStorageGetDecryptionKeyRequest(httpRequestContext, componentName, request) {
245
+ core.Guards.object(ROUTES_SOURCE, "request", request);
246
+ core.Guards.object(ROUTES_SOURCE, "request.body", request.body);
247
+ const component = core.ComponentFactory.get(componentName);
248
+ const key = await component.getDecryptionKey(request.body.nodeIdentity, request.body.proof);
249
+ return {
250
+ body: {
251
+ decryptionKey: key
252
+ }
253
+ };
254
+ }
145
255
 
146
256
  const restEntryPoints = [
147
257
  {
@@ -161,6 +271,208 @@ function initSchema() {
161
271
  entity.EntitySchemaFactory.register("SyncSnapshotEntry", () => entity.EntitySchemaHelper.getSchema(exports.SyncSnapshotEntry));
162
272
  }
163
273
 
274
+ var mainnet = "";
275
+ var testnet = "";
276
+ var devnet = "";
277
+ var verifiableStorageKeys = {
278
+ mainnet: mainnet,
279
+ testnet: testnet,
280
+ devnet: devnet
281
+ };
282
+
283
+ /**
284
+ * Class for performing blob storage operations.
285
+ */
286
+ class BlobStorageHelper {
287
+ /**
288
+ * Runtime name for the class.
289
+ */
290
+ CLASS_NAME = "BlobStorageHelper";
291
+ /**
292
+ * The logging connector to use for logging.
293
+ * @internal
294
+ */
295
+ _logging;
296
+ /**
297
+ * The vault connector.
298
+ * @internal
299
+ */
300
+ _vaultConnector;
301
+ /**
302
+ * The blob storage connector to use.
303
+ * @internal
304
+ */
305
+ _blobStorageConnector;
306
+ /**
307
+ * The id of the vault key to use for encrypting/decrypting blobs.
308
+ * @internal
309
+ */
310
+ _blobStorageEncryptionKeyId;
311
+ /**
312
+ * Is this a trusted node.
313
+ * @internal
314
+ */
315
+ _isTrustedNode;
316
+ /**
317
+ * Create a new instance of BlobStorageHelper.
318
+ * @param logging The logging connector to use for logging.
319
+ * @param vaultConnector The vault connector to use for for the encryption key.
320
+ * @param blobStorageConnector The blob storage component to use.
321
+ * @param blobStorageEncryptionKeyId The id of the vault key to use for encrypting/decrypting blobs.
322
+ * @param isTrustedNode Is this a trusted node.
323
+ */
324
+ constructor(logging, vaultConnector, blobStorageConnector, blobStorageEncryptionKeyId, isTrustedNode) {
325
+ this._logging = logging;
326
+ this._vaultConnector = vaultConnector;
327
+ this._blobStorageConnector = blobStorageConnector;
328
+ this._blobStorageEncryptionKeyId = blobStorageEncryptionKeyId;
329
+ this._isTrustedNode = isTrustedNode;
330
+ }
331
+ /**
332
+ * Load a blob from storage.
333
+ * @param blobId The id of the blob to apply.
334
+ * @returns The blob.
335
+ */
336
+ async loadBlob(blobId) {
337
+ await this._logging?.log({
338
+ level: "info",
339
+ source: this.CLASS_NAME,
340
+ message: "loadBlob",
341
+ data: {
342
+ blobId
343
+ }
344
+ });
345
+ try {
346
+ const encryptedBlob = await this._blobStorageConnector.get(blobId);
347
+ if (core.Is.uint8Array(encryptedBlob)) {
348
+ let compressedBlob;
349
+ // If this is a trusted node, we can decrypt the blob using the vault
350
+ if (this._isTrustedNode) {
351
+ compressedBlob = await this._vaultConnector.decrypt(this._blobStorageEncryptionKeyId, vaultModels.VaultEncryptionType.Rsa2048, encryptedBlob);
352
+ }
353
+ else {
354
+ // Otherwise we need the public key stored as a secret in the vault
355
+ const key = await this._vaultConnector.getSecret(this._blobStorageEncryptionKeyId);
356
+ const rsa = new crypto.RSA(core.Converter.base64ToBytes(key));
357
+ compressedBlob = rsa.decrypt(encryptedBlob);
358
+ }
359
+ const decompressedBlob = await core.Compression.decompress(compressedBlob, core.CompressionType.Gzip);
360
+ await this._logging?.log({
361
+ level: "info",
362
+ source: this.CLASS_NAME,
363
+ message: "loadedBlob",
364
+ data: {
365
+ blobId
366
+ }
367
+ });
368
+ return core.ObjectHelper.fromBytes(decompressedBlob);
369
+ }
370
+ }
371
+ catch (error) {
372
+ await this._logging?.log({
373
+ level: "error",
374
+ source: this.CLASS_NAME,
375
+ message: "loadBlobFailed",
376
+ data: {
377
+ blobId
378
+ },
379
+ error: core.BaseError.fromError(error)
380
+ });
381
+ }
382
+ await this._logging?.log({
383
+ level: "info",
384
+ source: this.CLASS_NAME,
385
+ message: "loadBlobEmpty",
386
+ data: {
387
+ blobId
388
+ }
389
+ });
390
+ }
391
+ /**
392
+ * Save a blob.
393
+ * @param blob The blob to save.
394
+ * @returns The id of the blob.
395
+ */
396
+ async saveBlob(blob) {
397
+ await this._logging?.log({
398
+ level: "info",
399
+ source: this.CLASS_NAME,
400
+ message: "saveBlob"
401
+ });
402
+ if (!this._isTrustedNode) {
403
+ throw new core.GeneralError(this.CLASS_NAME, "notTrustedNode");
404
+ }
405
+ const compressedBlob = await core.Compression.compress(core.ObjectHelper.toBytes(blob), core.CompressionType.Gzip);
406
+ const encryptedBlob = await this._vaultConnector.encrypt(this._blobStorageEncryptionKeyId, vaultModels.VaultEncryptionType.Rsa2048, compressedBlob);
407
+ try {
408
+ const blobId = await this._blobStorageConnector.set(encryptedBlob);
409
+ await this._logging?.log({
410
+ level: "info",
411
+ source: this.CLASS_NAME,
412
+ message: "savedBlob",
413
+ data: {
414
+ blobId
415
+ }
416
+ });
417
+ return blobId;
418
+ }
419
+ catch (error) {
420
+ await this._logging?.log({
421
+ level: "error",
422
+ source: this.CLASS_NAME,
423
+ message: "saveBlobFailed",
424
+ error: core.BaseError.fromError(error)
425
+ });
426
+ throw error;
427
+ }
428
+ }
429
+ /**
430
+ * Remove a blob from storage.
431
+ * @param blobId The id of the blob to remove.
432
+ * @returns Nothing.
433
+ */
434
+ async removeBlob(blobId) {
435
+ await this._logging?.log({
436
+ level: "info",
437
+ source: this.CLASS_NAME,
438
+ message: "removeBlob",
439
+ data: {
440
+ blobId
441
+ }
442
+ });
443
+ try {
444
+ await this._blobStorageConnector.remove(blobId);
445
+ await this._logging?.log({
446
+ level: "info",
447
+ source: this.CLASS_NAME,
448
+ message: "removedBlob",
449
+ data: {
450
+ blobId
451
+ }
452
+ });
453
+ }
454
+ catch (error) {
455
+ await this._logging?.log({
456
+ level: "error",
457
+ source: this.CLASS_NAME,
458
+ message: "removeBlobFailed",
459
+ data: {
460
+ blobId
461
+ },
462
+ error: core.BaseError.fromError(error)
463
+ });
464
+ }
465
+ await this._logging?.log({
466
+ level: "info",
467
+ source: this.CLASS_NAME,
468
+ message: "removeBlobEmpty",
469
+ data: {
470
+ blobId
471
+ }
472
+ });
473
+ }
474
+ }
475
+
164
476
  // Copyright 2024 IOTA Stiftung.
165
477
  // SPDX-License-Identifier: Apache-2.0.
166
478
  /**
@@ -182,10 +494,10 @@ class ChangeSetHelper {
182
494
  */
183
495
  _eventBusComponent;
184
496
  /**
185
- * The blob storage component to use for remote sync states.
497
+ * The blob storage helper to use for remote sync states.
186
498
  * @internal
187
499
  */
188
- _blobStorageComponent;
500
+ _blobStorageHelper;
189
501
  /**
190
502
  * The identity connector to use for signing/verifying changesets.
191
503
  * @internal
@@ -196,37 +508,73 @@ class ChangeSetHelper {
196
508
  * @internal
197
509
  */
198
510
  _decentralisedStorageMethodId;
511
+ /**
512
+ * The identity of the node that is performing the update.
513
+ * @internal
514
+ */
515
+ _nodeIdentity;
199
516
  /**
200
517
  * Create a new instance of ChangeSetHelper.
201
518
  * @param logging The logging connector to use for logging.
202
519
  * @param eventBusComponent The event bus component to use for events.
203
- * @param blobStorageComponent The blob storage component to use for remote sync states.
204
520
  * @param identityConnector The identity connector to use for signing/verifying changesets.
521
+ * @param blobStorageHelper The blob storage component to use for remote sync states.
205
522
  * @param decentralisedStorageMethodId The id of the identity method to use when signing/verifying changesets.
206
523
  */
207
- constructor(logging, eventBusComponent, blobStorageComponent, identityConnector, decentralisedStorageMethodId) {
524
+ constructor(logging, eventBusComponent, identityConnector, blobStorageHelper, decentralisedStorageMethodId) {
208
525
  this._logging = logging;
209
526
  this._eventBusComponent = eventBusComponent;
210
527
  this._decentralisedStorageMethodId = decentralisedStorageMethodId;
211
- this._blobStorageComponent = blobStorageComponent;
528
+ this._blobStorageHelper = blobStorageHelper;
212
529
  this._identityConnector = identityConnector;
213
530
  }
531
+ /**
532
+ * Set the node identity to use for signing changesets.
533
+ * @param nodeIdentity The identity of the node that is performing the update.
534
+ */
535
+ setNodeIdentity(nodeIdentity) {
536
+ this._nodeIdentity = nodeIdentity;
537
+ }
214
538
  /**
215
539
  * Get and verify a changeset.
216
540
  * @param changeSetStorageId The id of the sync changeset to apply.
217
541
  * @returns The changeset if it was verified.
218
542
  */
219
543
  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
544
+ await this._logging?.log({
545
+ level: "info",
546
+ source: this.CLASS_NAME,
547
+ message: "getChangeSet",
548
+ data: {
549
+ changeSetStorageId
550
+ }
224
551
  });
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;
552
+ try {
553
+ const syncChangeSet = await this._blobStorageHelper.loadBlob(changeSetStorageId);
554
+ if (core.Is.object(syncChangeSet)) {
555
+ const verified = await this.verifyChangesetProof(syncChangeSet);
556
+ return verified ? syncChangeSet : undefined;
557
+ }
558
+ }
559
+ catch (error) {
560
+ await this._logging?.log({
561
+ level: "warn",
562
+ source: this.CLASS_NAME,
563
+ message: "getChangeSetError",
564
+ data: {
565
+ changeSetStorageId
566
+ },
567
+ error: core.BaseError.fromError(error)
568
+ });
229
569
  }
570
+ await this._logging?.log({
571
+ level: "info",
572
+ source: this.CLASS_NAME,
573
+ message: "getChangeSetEmpty",
574
+ data: {
575
+ changeSetStorageId
576
+ }
577
+ });
230
578
  }
231
579
  /**
232
580
  * Apply a sync changeset.
@@ -235,7 +583,9 @@ class ChangeSetHelper {
235
583
  */
236
584
  async getAndApplyChangeset(changeSetStorageId) {
237
585
  const syncChangeset = await this.getAndVerifyChangeset(changeSetStorageId);
238
- if (!core.Is.empty(syncChangeset)) {
586
+ // Only apply changesets from other nodes, we don't want to overwrite
587
+ // any changes we have made to local entity storage
588
+ if (!core.Is.empty(syncChangeset) && syncChangeset.nodeIdentity !== this._nodeIdentity) {
239
589
  await this.applyChangeset(syncChangeset);
240
590
  }
241
591
  return syncChangeset;
@@ -260,13 +610,18 @@ class ChangeSetHelper {
260
610
  switch (change.operation) {
261
611
  case synchronisedStorageModels.SyncChangeOperation.Set:
262
612
  if (!core.Is.empty(change.entity)) {
263
- // The node identity was stripped when stored in the changeset
613
+ // The id was stripped from the entity as it is part of the operation
614
+ // we make sure we reinstate it in the publish
615
+ // Also the node identity was stripped when stored in the changeset
264
616
  // as the changeset is signed with the node identity.
265
617
  // so we need to restore it here.
266
- change.entity.nodeIdentity = syncChangeset.nodeIdentity;
267
618
  await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.RemoteItemSet, {
268
619
  storageKey: syncChangeset.storageKey,
269
- entity: change.entity
620
+ entity: {
621
+ ...change.entity,
622
+ id: change.id,
623
+ nodeIdentity: syncChangeset.nodeIdentity
624
+ }
270
625
  });
271
626
  }
272
627
  break;
@@ -274,7 +629,8 @@ class ChangeSetHelper {
274
629
  if (!core.Is.empty(change.id)) {
275
630
  await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.RemoteItemRemove, {
276
631
  storageKey: syncChangeset.storageKey,
277
- id: change.id
632
+ id: change.id,
633
+ nodeIdentity: syncChangeset.nodeIdentity
278
634
  });
279
635
  }
280
636
  break;
@@ -285,10 +641,9 @@ class ChangeSetHelper {
285
641
  /**
286
642
  * Store the changeset.
287
643
  * @param syncChangeSet The sync change set to store.
288
- * @param nodeIdentity The node identity to use for the changeset.
289
644
  * @returns The id of the change set.
290
645
  */
291
- async storeChangeSet(syncChangeSet, nodeIdentity) {
646
+ async storeChangeSet(syncChangeSet) {
292
647
  await this._logging?.log({
293
648
  level: "info",
294
649
  source: this.CLASS_NAME,
@@ -297,12 +652,7 @@ class ChangeSetHelper {
297
652
  id: syncChangeSet.id
298
653
  }
299
654
  });
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);
655
+ return this._blobStorageHelper.saveBlob(syncChangeSet);
306
656
  }
307
657
  /**
308
658
  * Verify the proof of a sync changeset.
@@ -321,6 +671,32 @@ class ChangeSetHelper {
321
671
  });
322
672
  return false;
323
673
  }
674
+ // If the proof or verification method is missing, the proof is invalid
675
+ const verificationMethod = syncChangeset.proof?.verificationMethod;
676
+ if (!core.Is.stringValue(verificationMethod)) {
677
+ await this._logging?.log({
678
+ level: "error",
679
+ source: this.CLASS_NAME,
680
+ message: "verifyChangeSetProofMissing",
681
+ data: {
682
+ id: syncChangeset.id
683
+ }
684
+ });
685
+ }
686
+ // Parse the verification method and extract the node identity
687
+ // this should match the node identity of the changeset
688
+ // otherwise you could sign a changeset for another node
689
+ const changeSetNodeIdentity = identityModels.DocumentHelper.parseId(verificationMethod ?? "");
690
+ if (changeSetNodeIdentity.id !== syncChangeset.nodeIdentity) {
691
+ await this._logging?.log({
692
+ level: "error",
693
+ source: this.CLASS_NAME,
694
+ message: "verifyChangeSetProofNodeIdentityMismatch",
695
+ data: {
696
+ id: syncChangeset.id
697
+ }
698
+ });
699
+ }
324
700
  const changeSetWithoutProof = core.ObjectHelper.clone(syncChangeset);
325
701
  delete changeSetWithoutProof.proof;
326
702
  const isValid = await this._identityConnector.verifyProof(changeSetWithoutProof, syncChangeset.proof);
@@ -352,9 +728,10 @@ class ChangeSetHelper {
352
728
  * @returns The proof.
353
729
  */
354
730
  async createChangeSetProof(syncChangeset) {
731
+ core.Guards.stringValue(this.CLASS_NAME, "nodeIdentity", this._nodeIdentity);
355
732
  const changeSetWithoutProof = core.ObjectHelper.clone(syncChangeset);
356
733
  delete changeSetWithoutProof.proof;
357
- const proof = await this._identityConnector.createProof(syncChangeset.nodeIdentity, identityModels.DocumentHelper.joinId(syncChangeset.nodeIdentity, this._decentralisedStorageMethodId), standardsW3cDid.ProofTypes.DataIntegrityProof, changeSetWithoutProof);
734
+ const proof = await this._identityConnector.createProof(this._nodeIdentity, identityModels.DocumentHelper.joinId(this._nodeIdentity, this._decentralisedStorageMethodId), standardsW3cDid.ProofTypes.DataIntegrityProof, changeSetWithoutProof);
358
735
  await this._logging?.log({
359
736
  level: "info",
360
737
  source: this.CLASS_NAME,
@@ -366,8 +743,65 @@ class ChangeSetHelper {
366
743
  });
367
744
  return proof;
368
745
  }
746
+ /**
747
+ * Copy a change set.
748
+ * @param syncChangeSet The sync changeset to copy.
749
+ * @returns The id of the updated change set.
750
+ */
751
+ async copyChangeset(syncChangeSet) {
752
+ if (core.Is.stringValue(this._nodeIdentity)) {
753
+ const verified = await this.verifyChangesetProof(syncChangeSet);
754
+ if (verified) {
755
+ await this._logging?.log({
756
+ level: "info",
757
+ source: this.CLASS_NAME,
758
+ message: "copyChangeSet",
759
+ data: {
760
+ changeSetStorageId: syncChangeSet.id
761
+ }
762
+ });
763
+ // Allocate a new id to the changeset copy and re-create a proof using this nodes identity
764
+ const copy = core.ObjectHelper.clone(syncChangeSet);
765
+ copy.id = core.Converter.bytesToHex(core.RandomHelper.generate(32));
766
+ copy.proof = await this.createChangeSetProof(copy);
767
+ // Store the copy
768
+ return {
769
+ syncChangeSet: copy,
770
+ changeSetStorageId: await this.storeChangeSet(copy)
771
+ };
772
+ }
773
+ }
774
+ }
775
+ /**
776
+ * Reset the storage for a given storage key.
777
+ * @param storageKey The key of the storage to reset.
778
+ * @param resetMode The reset mode, this will use the nodeIdentity in the entities to determine which are local/remote.
779
+ * @returns Nothing.
780
+ */
781
+ async reset(storageKey, resetMode) {
782
+ // If we are applying a consolidation we need to reset the local db
783
+ // but keep any entries from the local node, as they might have been updated
784
+ await this._logging?.log({
785
+ level: "info",
786
+ source: this.CLASS_NAME,
787
+ message: "storageReset",
788
+ data: {
789
+ storageKey
790
+ }
791
+ });
792
+ await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.Reset, {
793
+ storageKey,
794
+ resetMode
795
+ });
796
+ }
369
797
  }
370
798
 
799
+ // Copyright 2024 IOTA Stiftung.
800
+ // SPDX-License-Identifier: Apache-2.0.
801
+ const SYNC_STATE_VERSION = "1";
802
+ const SYNC_POINTER_STORE_VERSION = "1";
803
+ const SYNC_SNAPSHOT_VERSION = "1";
804
+
371
805
  // Copyright 2024 IOTA Stiftung.
372
806
  // SPDX-License-Identifier: Apache-2.0.
373
807
  /**
@@ -387,7 +821,7 @@ class LocalSyncStateHelper {
387
821
  * The storage connector for the sync snapshot entries.
388
822
  * @internal
389
823
  */
390
- _localSyncSnapshotEntryEntityStorage;
824
+ _snapshotEntryEntityStorage;
391
825
  /**
392
826
  * The change set helper to use for applying changesets.
393
827
  * @internal
@@ -396,12 +830,12 @@ class LocalSyncStateHelper {
396
830
  /**
397
831
  * Create a new instance of LocalSyncStateHelper.
398
832
  * @param logging The logging connector to use for logging.
399
- * @param localSyncSnapshotEntryEntityStorage The storage connector for the local sync snapshot entries.
833
+ * @param snapshotEntryEntityStorage The storage connector for the sync snapshot entries.
400
834
  * @param changeSetHelper The change set helper to use for applying changesets.
401
835
  */
402
- constructor(logging, localSyncSnapshotEntryEntityStorage, changeSetHelper) {
836
+ constructor(logging, snapshotEntryEntityStorage, changeSetHelper) {
403
837
  this._logging = logging;
404
- this._localSyncSnapshotEntryEntityStorage = localSyncSnapshotEntryEntityStorage;
838
+ this._snapshotEntryEntityStorage = snapshotEntryEntityStorage;
405
839
  this._changeSetHelper = changeSetHelper;
406
840
  }
407
841
  /**
@@ -422,40 +856,44 @@ class LocalSyncStateHelper {
422
856
  id
423
857
  }
424
858
  });
425
- const localChangeSnapshot = await this.getLocalChangeSnapshot(storageKey);
426
- localChangeSnapshot.changes ??= [];
427
- // If we already have a change for this id we are
428
- // about to supersede it, we remove the previous change
429
- // to avoid having multiple changes for the same id
430
- const previousChangeIndex = localChangeSnapshot.changes.findIndex(change => change.id === id);
431
- if (previousChangeIndex !== -1) {
432
- localChangeSnapshot.changes.splice(previousChangeIndex, 1);
433
- }
434
- if (localChangeSnapshot.changes.length > 0) {
435
- localChangeSnapshot.dateModified = new Date(Date.now()).toISOString();
859
+ const localChangeSnapshots = await this.getSnapshots(storageKey, true);
860
+ if (localChangeSnapshots.length > 0) {
861
+ const localChangeSnapshot = localChangeSnapshots[0];
862
+ localChangeSnapshot.changes ??= [];
863
+ // If we already have a change for this id we are
864
+ // about to supersede it, we remove the previous change
865
+ // to avoid having multiple changes for the same id
866
+ const previousChangeIndex = localChangeSnapshot.changes.findIndex(change => change.id === id);
867
+ if (previousChangeIndex !== -1) {
868
+ localChangeSnapshot.changes.splice(previousChangeIndex, 1);
869
+ }
870
+ if (localChangeSnapshot.changes.length > 0) {
871
+ localChangeSnapshot.dateModified = new Date(Date.now()).toISOString();
872
+ }
873
+ localChangeSnapshot.changes.push({ operation, id });
874
+ await this.setLocalChangeSnapshot(localChangeSnapshot);
436
875
  }
437
- localChangeSnapshot.changes.push({ operation, id });
438
- await this.setLocalChangeSnapshot(localChangeSnapshot);
439
876
  }
440
877
  /**
441
- * Get the current local snapshot.
878
+ * Get the snapshot which contains just the changes for this node.
442
879
  * @param storageKey The storage key of the snapshot to get.
880
+ * @param isLocal Whether to get the local snapshot or not.
443
881
  * @returns The local snapshot entry.
444
882
  */
445
- async getLocalChangeSnapshot(storageKey) {
883
+ async getSnapshots(storageKey, isLocal) {
446
884
  await this._logging?.log({
447
885
  level: "info",
448
886
  source: this.CLASS_NAME,
449
- message: "getLocalChangeSnapshot",
887
+ message: "getSnapshots",
450
888
  data: {
451
889
  storageKey
452
890
  }
453
891
  });
454
- const queryResult = await this._localSyncSnapshotEntryEntityStorage.query({
892
+ const queryResult = await this._snapshotEntryEntityStorage.query({
455
893
  conditions: [
456
894
  {
457
- property: "isLocalSnapshot",
458
- value: true,
895
+ property: "isLocal",
896
+ value: isLocal,
459
897
  comparison: entity.ComparisonOperator.Equals
460
898
  },
461
899
  {
@@ -469,31 +907,37 @@ class LocalSyncStateHelper {
469
907
  await this._logging?.log({
470
908
  level: "info",
471
909
  source: this.CLASS_NAME,
472
- message: "localChangeSnapshotExists",
910
+ message: "getSnapshotsExists",
473
911
  data: {
474
912
  storageKey
475
913
  }
476
914
  });
477
- return queryResult.entities[0];
915
+ return queryResult.entities;
478
916
  }
479
917
  await this._logging?.log({
480
918
  level: "info",
481
919
  source: this.CLASS_NAME,
482
- message: "localChangeSnapshotDoesNotExist",
920
+ message: "getSnapshotsDoesNotExist",
483
921
  data: {
484
922
  storageKey
485
923
  }
486
924
  });
487
- return {
488
- id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
489
- storageKey,
490
- dateCreated: new Date(Date.now()).toISOString(),
491
- changeSetStorageIds: [],
492
- isLocalSnapshot: true
493
- };
925
+ const now = new Date(Date.now()).toISOString();
926
+ return [
927
+ {
928
+ version: SYNC_SNAPSHOT_VERSION,
929
+ id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
930
+ storageKey,
931
+ dateCreated: now,
932
+ dateModified: now,
933
+ changeSetStorageIds: [],
934
+ isLocal,
935
+ isConsolidated: false
936
+ }
937
+ ];
494
938
  }
495
939
  /**
496
- * Set the current local snapshot.
940
+ * Set the current local snapshot with changes for this node.
497
941
  * @param localChangeSnapshot The local change snapshot to set.
498
942
  * @returns Nothing.
499
943
  */
@@ -506,10 +950,10 @@ class LocalSyncStateHelper {
506
950
  storageKey: localChangeSnapshot.storageKey
507
951
  }
508
952
  });
509
- await this._localSyncSnapshotEntryEntityStorage.set(localChangeSnapshot);
953
+ await this._snapshotEntryEntityStorage.set(localChangeSnapshot);
510
954
  }
511
955
  /**
512
- * Get the current local snapshot.
956
+ * Get the current local snapshot with the changes for this node.
513
957
  * @param localChangeSnapshot The local change snapshot to remove.
514
958
  * @returns Nothing.
515
959
  */
@@ -522,85 +966,155 @@ class LocalSyncStateHelper {
522
966
  snapshotId: localChangeSnapshot.id
523
967
  }
524
968
  });
525
- await this._localSyncSnapshotEntryEntityStorage.remove(localChangeSnapshot.id);
969
+ await this._snapshotEntryEntityStorage.remove(localChangeSnapshot.id);
526
970
  }
527
971
  /**
528
- * Sync local data using a remote sync state.
972
+ * Apply a sync state to the local node.
529
973
  * @param storageKey The storage key of the snapshot to sync with.
530
- * @param remoteSyncState The sync state to sync with.
974
+ * @param syncState The sync state to sync with.
531
975
  * @returns Nothing.
532
976
  */
533
- async syncFromRemote(storageKey, remoteSyncState) {
977
+ async applySyncState(storageKey, syncState) {
534
978
  await this._logging?.log({
535
979
  level: "info",
536
980
  source: this.CLASS_NAME,
537
- message: "remoteSyncSynchronisation",
981
+ message: "applySyncState",
538
982
  data: {
539
- snapshotCount: remoteSyncState.snapshots.length
983
+ snapshotCount: syncState.snapshots.length
540
984
  }
541
985
  });
986
+ // Get all the existing snapshots that we have processed previously
987
+ const existingRemoteSnapshots = await this.getSnapshots(storageKey, false);
542
988
  // Sort from newest to oldest
543
- const sortedRemoteSnapshots = remoteSyncState.snapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
544
- const newSnapshots = [];
545
- const modifiedSnapshots = [];
546
- for (const remoteSnapshot of sortedRemoteSnapshots) {
989
+ const sortedSnapshots = syncState.snapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
990
+ // If we have no existing snapshots we can't have yet synced
991
+ // in this case we need to find the most recent consolidation
992
+ // and use that to build a complete DB table
993
+ if (existingRemoteSnapshots.length === 0) {
547
994
  await this._logging?.log({
548
995
  level: "info",
549
996
  source: this.CLASS_NAME,
550
- message: "remoteSyncSnapshotProcessing",
997
+ message: "applySnapshotNoExisting",
551
998
  data: {
552
- snapshotId: remoteSnapshot.id,
553
- dateCreated: new Date(remoteSnapshot.dateCreated).toISOString()
999
+ storageKey
554
1000
  }
555
1001
  });
556
- const localSnapshot = await this._localSyncSnapshotEntryEntityStorage.get(remoteSnapshot.id);
557
- const remoteSnapshotWithContext = {
558
- ...remoteSnapshot,
559
- storageKey
560
- };
561
- if (core.Is.empty(localSnapshot)) {
562
- // We don't have the snapshot locally, so we need to process it
563
- newSnapshots.push(remoteSnapshotWithContext);
564
- }
565
- else if (localSnapshot.dateModified !== remoteSnapshot.dateModified) {
566
- // If the local snapshot has a different dateModified, we need to update it
567
- modifiedSnapshots.push({
568
- localSnapshot,
569
- remoteSnapshot: remoteSnapshotWithContext
1002
+ const firstConsolidated = sortedSnapshots.find(snapshot => snapshot.isConsolidated);
1003
+ if (firstConsolidated) {
1004
+ // We found a consolidated snapshot, we can use it
1005
+ await this._logging?.log({
1006
+ level: "info",
1007
+ source: this.CLASS_NAME,
1008
+ message: "applySnapshotFoundConsolidated",
1009
+ data: {
1010
+ storageKey,
1011
+ snapshotId: firstConsolidated.id
1012
+ }
570
1013
  });
1014
+ // We need to reset the entity storage and remove all the remote items
1015
+ // so that we use just the ones from the consolidation
1016
+ await this._changeSetHelper.reset(storageKey, synchronisedStorageModels.SyncNodeIdentityMode.Remote);
1017
+ await this.processNewSnapshots([
1018
+ {
1019
+ ...firstConsolidated,
1020
+ storageKey,
1021
+ isLocal: false
1022
+ }
1023
+ ]);
571
1024
  }
572
1025
  else {
573
- // we sorted the snapshots from newest to oldest, so if we found a local snapshot
574
- // with the same dateModified as the remote snapshot, we can stop processing further
575
- break;
1026
+ await this._logging?.log({
1027
+ level: "info",
1028
+ source: this.CLASS_NAME,
1029
+ message: "applySnapshotNoConsolidated",
1030
+ data: {
1031
+ storageKey
1032
+ }
1033
+ });
1034
+ }
1035
+ }
1036
+ else {
1037
+ // Create a lookup map for the existing snapshots
1038
+ const existingSnapshots = {};
1039
+ for (const snapshot of existingRemoteSnapshots) {
1040
+ existingSnapshots[snapshot.id] = snapshot;
1041
+ }
1042
+ const newSnapshots = [];
1043
+ const modifiedSnapshots = [];
1044
+ const referencedExistingSnapshots = Object.keys(existingSnapshots);
1045
+ for (const snapshot of sortedSnapshots) {
1046
+ await this._logging?.log({
1047
+ level: "info",
1048
+ source: this.CLASS_NAME,
1049
+ message: "applySnapshot",
1050
+ data: {
1051
+ snapshotId: snapshot.id,
1052
+ dateCreated: new Date(snapshot.dateCreated).toISOString()
1053
+ }
1054
+ });
1055
+ // See if we have the local snapshot
1056
+ const currentSnapshot = existingSnapshots[snapshot.id];
1057
+ // As we are referencing an existing snapshot, we need to remove it from the list
1058
+ // to allow us to cleanup any unreferenced snapshots later
1059
+ const idx = referencedExistingSnapshots.indexOf(snapshot.id);
1060
+ if (idx !== -1) {
1061
+ referencedExistingSnapshots.splice(idx, 1);
1062
+ }
1063
+ const updatedSnapshot = {
1064
+ ...snapshot,
1065
+ storageKey,
1066
+ isLocal: false
1067
+ };
1068
+ if (core.Is.empty(currentSnapshot)) {
1069
+ // We don't have the snapshot locally, so we need to process it
1070
+ newSnapshots.push(updatedSnapshot);
1071
+ }
1072
+ else if (currentSnapshot.dateModified !== snapshot.dateModified) {
1073
+ // If the local snapshot has a different dateModified, we need to update it
1074
+ modifiedSnapshots.push({
1075
+ currentSnapshot,
1076
+ updatedSnapshot
1077
+ });
1078
+ }
1079
+ else {
1080
+ // we sorted the snapshots from newest to oldest, so if we found a local snapshot
1081
+ // with the same dateModified as the remote snapshot, we can stop processing further
1082
+ break;
1083
+ }
1084
+ }
1085
+ // We reverse the order of the snapshots to process them from oldest to newest
1086
+ // because we want to apply the changes in the order they were created
1087
+ await this.processModifiedSnapshots(modifiedSnapshots.reverse());
1088
+ await this.processNewSnapshots(newSnapshots.reverse());
1089
+ // Any ids remaining in this list are no longer referenced in the global state
1090
+ // so we should remove them from the local storage as they will never be updated again
1091
+ for (const referencedSnapshotId of referencedExistingSnapshots) {
1092
+ await this._snapshotEntryEntityStorage.remove(referencedSnapshotId);
576
1093
  }
577
1094
  }
578
- // We reverse the order of the snapshots to process them from oldest to newest
579
- // because we want to apply the changes in the order they were created
580
- await this.processModifiedSnapshots(modifiedSnapshots.reverse());
581
- await this.processNewSnapshots(newSnapshots.reverse());
582
1095
  }
583
1096
  /**
584
1097
  * Process the modified snapshots and store them in the local storage.
585
1098
  * @param modifiedSnapshots The modified snapshots to process.
586
1099
  * @returns Nothing.
1100
+ * @internal
587
1101
  */
588
1102
  async processModifiedSnapshots(modifiedSnapshots) {
589
1103
  for (const modifiedSnapshot of modifiedSnapshots) {
590
1104
  await this._logging?.log({
591
1105
  level: "info",
592
1106
  source: this.CLASS_NAME,
593
- message: "remoteSyncSnapshotModified",
1107
+ message: "processModifiedSnapshot",
594
1108
  data: {
595
- snapshotId: modifiedSnapshot.remoteSnapshot.id,
596
- localModified: new Date(modifiedSnapshot.localSnapshot.dateModified ??
597
- modifiedSnapshot.localSnapshot.dateCreated).toISOString(),
598
- remoteModified: new Date(modifiedSnapshot.remoteSnapshot.dateModified ??
599
- modifiedSnapshot.remoteSnapshot.dateCreated).toISOString()
1109
+ snapshotId: modifiedSnapshot.updatedSnapshot.id,
1110
+ localModified: new Date(modifiedSnapshot.currentSnapshot.dateModified ??
1111
+ modifiedSnapshot.currentSnapshot.dateCreated).toISOString(),
1112
+ remoteModified: new Date(modifiedSnapshot.updatedSnapshot.dateModified ??
1113
+ modifiedSnapshot.updatedSnapshot.dateCreated).toISOString()
600
1114
  }
601
1115
  });
602
- const remoteChangeSetStorageIds = modifiedSnapshot.remoteSnapshot.changeSetStorageIds;
603
- const localChangeSetStorageIds = modifiedSnapshot.localSnapshot.changeSetStorageIds ?? [];
1116
+ const remoteChangeSetStorageIds = modifiedSnapshot.updatedSnapshot.changeSetStorageIds;
1117
+ const localChangeSetStorageIds = modifiedSnapshot.currentSnapshot.changeSetStorageIds ?? [];
604
1118
  if (core.Is.arrayValue(remoteChangeSetStorageIds)) {
605
1119
  for (const storageId of remoteChangeSetStorageIds) {
606
1120
  // Check if the local snapshot does not have the storageId
@@ -609,23 +1123,24 @@ class LocalSyncStateHelper {
609
1123
  }
610
1124
  }
611
1125
  }
612
- await this._localSyncSnapshotEntryEntityStorage.set(modifiedSnapshot.remoteSnapshot);
1126
+ await this._snapshotEntryEntityStorage.set(modifiedSnapshot.updatedSnapshot);
613
1127
  }
614
1128
  }
615
1129
  /**
616
1130
  * Process the new snapshots and store them in the local storage.
617
1131
  * @param newSnapshots The new snapshots to process.
618
1132
  * @returns Nothing.
1133
+ * @internal
619
1134
  */
620
1135
  async processNewSnapshots(newSnapshots) {
621
1136
  for (const newSnapshot of newSnapshots) {
622
1137
  await this._logging?.log({
623
1138
  level: "info",
624
1139
  source: this.CLASS_NAME,
625
- message: "remoteSyncSnapshotNew",
1140
+ message: "processNewSnapshot",
626
1141
  data: {
627
1142
  snapshotId: newSnapshot.id,
628
- localModified: new Date(newSnapshot.dateCreated).toISOString()
1143
+ dateCreated: newSnapshot.dateCreated
629
1144
  }
630
1145
  });
631
1146
  const newSnapshotChangeSetStorageIds = newSnapshot.changeSetStorageIds ?? [];
@@ -634,7 +1149,7 @@ class LocalSyncStateHelper {
634
1149
  await this._changeSetHelper.getAndApplyChangeset(storageId);
635
1150
  }
636
1151
  }
637
- await this._localSyncSnapshotEntryEntityStorage.set(newSnapshot);
1152
+ await this._snapshotEntryEntityStorage.set(newSnapshot);
638
1153
  }
639
1154
  }
640
1155
  }
@@ -660,10 +1175,10 @@ class RemoteSyncStateHelper {
660
1175
  */
661
1176
  _eventBusComponent;
662
1177
  /**
663
- * The blob storage component to use for remote sync states.
1178
+ * The blob storage helper.
664
1179
  * @internal
665
1180
  */
666
- _blobStorageComponent;
1181
+ _blobStorageHelper;
667
1182
  /**
668
1183
  * The verifiable storage connector to use for storing sync pointers.
669
1184
  * @internal
@@ -694,22 +1209,34 @@ class RemoteSyncStateHelper {
694
1209
  * @internal
695
1210
  */
696
1211
  _nodeIdentity;
1212
+ /**
1213
+ * Whether the node is trusted or not.
1214
+ * @internal
1215
+ */
1216
+ _isTrustedNode;
1217
+ /**
1218
+ * Maximum number of consolidations to keep in storage.
1219
+ * @internal
1220
+ */
1221
+ _maxConsolidations;
697
1222
  /**
698
1223
  * Create a new instance of DecentralisedEntityStorageConnector.
699
1224
  * @param logging The logging connector to use for logging.
700
1225
  * @param eventBusComponent The event bus component to use for events.
701
- * @param blobStorageComponent The blob storage component to use for remote sync states.
702
1226
  * @param verifiableSyncPointerStorageConnector The verifiable storage connector to use for storing sync pointers.
1227
+ * @param blobStorageHelper The blob storage helper to use for remote sync states.
703
1228
  * @param changeSetHelper The change set helper to use for managing changesets.
704
- * @param synchronisedStorageKey The synchronised storage key to use for verified storage operations.
1229
+ * @param isTrustedNode Whether the node is trusted or not.
1230
+ * @param maxConsolidations The maximum number of consolidations to keep in storage.
705
1231
  */
706
- constructor(logging, eventBusComponent, blobStorageComponent, verifiableSyncPointerStorageConnector, changeSetHelper, synchronisedStorageKey) {
1232
+ constructor(logging, eventBusComponent, verifiableSyncPointerStorageConnector, blobStorageHelper, changeSetHelper, isTrustedNode, maxConsolidations) {
707
1233
  this._logging = logging;
708
1234
  this._eventBusComponent = eventBusComponent;
709
- this._blobStorageComponent = blobStorageComponent;
710
1235
  this._verifiableSyncPointerStorageConnector = verifiableSyncPointerStorageConnector;
711
1236
  this._changeSetHelper = changeSetHelper;
712
- this._synchronisedStorageKey = synchronisedStorageKey;
1237
+ this._blobStorageHelper = blobStorageHelper;
1238
+ this._isTrustedNode = isTrustedNode;
1239
+ this._maxConsolidations = maxConsolidations;
713
1240
  this._batchResponseStorageIds = {};
714
1241
  this._populateFullChanges = {};
715
1242
  this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.BatchResponse, async (response) => {
@@ -727,17 +1254,24 @@ class RemoteSyncStateHelper {
727
1254
  this._nodeIdentity = nodeIdentity;
728
1255
  }
729
1256
  /**
730
- * Create and store a change set.
1257
+ * Set the synchronised storage key.
1258
+ * @param synchronisedStorageKey The synchronised storage key to use.
1259
+ */
1260
+ setSynchronisedStorageKey(synchronisedStorageKey) {
1261
+ this._synchronisedStorageKey = synchronisedStorageKey;
1262
+ }
1263
+ /**
1264
+ * Build a changeset.
731
1265
  * @param storageKey The storage key of the change set.
732
1266
  * @param changes The changes to apply.
733
1267
  * @param completeCallback The callback to call when the changeset is created and stored.
734
1268
  * @returns The storage id of the change set if created.
735
1269
  */
736
- async createAndStoreChangeSet(storageKey, changes, completeCallback) {
1270
+ async buildChangeSet(storageKey, changes, completeCallback) {
737
1271
  await this._logging?.log({
738
1272
  level: "info",
739
1273
  source: this.CLASS_NAME,
740
- message: "createAndStoreChangeSet",
1274
+ message: "buildingChangeSet",
741
1275
  data: {
742
1276
  storageKey,
743
1277
  changeCount: changes.length
@@ -796,15 +1330,19 @@ class RemoteSyncStateHelper {
796
1330
  for (const change of changes) {
797
1331
  change.entity = this._populateFullChanges[storageKey].entities[change.id] ?? change.entity;
798
1332
  if (change.operation === synchronisedStorageModels.SyncChangeOperation.Set && core.Is.objectValue(change.entity)) {
1333
+ // Remove the id from the entity as this is stored in the operation
1334
+ // and will be reinstated when the changeset is reconstituted
1335
+ core.ObjectHelper.propertyDelete(change.entity, "id");
799
1336
  // Remove the node identity as the changeset has this stored at the top level
800
1337
  // and we do not want to store it in the change itself to reduce redundancy
801
1338
  core.ObjectHelper.propertyDelete(change.entity, "nodeIdentity");
802
1339
  }
803
1340
  }
804
- // Add the changeset to the current snapshot
1341
+ const now = new Date(Date.now()).toISOString();
805
1342
  const syncChangeSet = {
806
1343
  id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
807
- dateCreated: new Date(Date.now()).toISOString(),
1344
+ dateCreated: now,
1345
+ dateModified: now,
808
1346
  storageKey,
809
1347
  changes,
810
1348
  nodeIdentity: this._nodeIdentity
@@ -812,9 +1350,12 @@ class RemoteSyncStateHelper {
812
1350
  try {
813
1351
  // And sign it with the node identity
814
1352
  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);
1353
+ // If this is a trusted node, we also store the changeset
1354
+ let changeSetStorageId;
1355
+ if (this._isTrustedNode) {
1356
+ changeSetStorageId = await this._changeSetHelper.storeChangeSet(syncChangeSet);
1357
+ }
1358
+ await completeCallback(syncChangeSet, changeSetStorageId);
818
1359
  }
819
1360
  catch (err) {
820
1361
  await this._logging?.log({
@@ -853,27 +1394,33 @@ class RemoteSyncStateHelper {
853
1394
  const syncPointerStore = await this.getVerifiableSyncPointerStore();
854
1395
  let syncState;
855
1396
  if (!core.Is.empty(syncPointerStore.syncPointers[storageKey])) {
856
- syncState = await this.getRemoteSyncState(syncPointerStore.syncPointers[storageKey]);
1397
+ syncState = await this.getSyncState(syncPointerStore.syncPointers[storageKey]);
857
1398
  }
858
1399
  // No current sync state, so we create a new one
859
1400
  if (core.Is.empty(syncState)) {
860
- syncState = { snapshots: [] };
1401
+ syncState = { version: SYNC_STATE_VERSION, storageKey, snapshots: [] };
861
1402
  }
862
1403
  // Sort the snapshots so the newest snapshot is last in the array
863
1404
  const sortedSnapshots = syncState.snapshots.sort((a, b) => a.dateCreated.localeCompare(b.dateCreated));
864
1405
  // Get the current snapshot, if it does not exist we create a new one
865
1406
  let currentSnapshot = sortedSnapshots[sortedSnapshots.length - 1];
866
- if (core.Is.empty(currentSnapshot)) {
1407
+ const now = new Date(Date.now()).toISOString();
1408
+ // If there is no snapshot or the current one is a consolidation
1409
+ // we start a new snapshot
1410
+ if (core.Is.empty(currentSnapshot) || currentSnapshot.isConsolidated) {
867
1411
  currentSnapshot = {
1412
+ version: SYNC_SNAPSHOT_VERSION,
868
1413
  id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
869
- dateCreated: new Date(Date.now()).toISOString(),
1414
+ dateCreated: now,
1415
+ dateModified: now,
1416
+ isConsolidated: false,
870
1417
  changeSetStorageIds: []
871
1418
  };
872
1419
  syncState.snapshots.push(currentSnapshot);
873
1420
  }
874
1421
  else {
875
1422
  // Snapshot exists, we update the dateModified
876
- currentSnapshot.dateModified = new Date(Date.now()).toISOString();
1423
+ currentSnapshot.dateModified = now;
877
1424
  }
878
1425
  // Add the changeset storage id to the current snapshot
879
1426
  currentSnapshot.changeSetStorageIds.push(changeSetStorageId);
@@ -888,57 +1435,61 @@ class RemoteSyncStateHelper {
888
1435
  * @param batchSize The batch size to use for consolidation.
889
1436
  * @returns Nothing.
890
1437
  */
891
- async consolidateFromLocal(storageKey, batchSize) {
1438
+ async consolidationStart(storageKey, batchSize) {
892
1439
  await this._logging?.log({
893
1440
  level: "info",
894
1441
  source: this.CLASS_NAME,
895
1442
  message: "consolidationStarting"
896
1443
  });
897
- await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.BatchRequest, { storageKey, batchSize });
1444
+ // Perform a batch request to start the consolidation
1445
+ await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.BatchRequest, { storageKey, batchSize, requestMode: synchronisedStorageModels.SyncNodeIdentityMode.All });
898
1446
  }
899
1447
  /**
900
1448
  * Get the sync pointer store.
901
1449
  * @returns The sync pointer store.
902
1450
  */
903
1451
  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);
1452
+ if (core.Is.stringValue(this._synchronisedStorageKey)) {
1453
+ try {
916
1454
  await this._logging?.log({
917
1455
  level: "info",
918
1456
  source: this.CLASS_NAME,
919
- message: "verifiableSyncPointerStoreRetrieved",
1457
+ message: "verifiableSyncPointerStoreRetrieving",
920
1458
  data: {
921
1459
  key: this._synchronisedStorageKey
922
1460
  }
923
1461
  });
924
- return syncPointer;
1462
+ const syncPointerStore = await this._verifiableSyncPointerStorageConnector.get(this._synchronisedStorageKey, { includeData: true });
1463
+ if (core.Is.uint8Array(syncPointerStore.data)) {
1464
+ const syncPointer = core.ObjectHelper.fromBytes(syncPointerStore.data);
1465
+ await this._logging?.log({
1466
+ level: "info",
1467
+ source: this.CLASS_NAME,
1468
+ message: "verifiableSyncPointerStoreRetrieved",
1469
+ data: {
1470
+ key: this._synchronisedStorageKey
1471
+ }
1472
+ });
1473
+ return syncPointer;
1474
+ }
925
1475
  }
926
- }
927
- catch (err) {
928
- if (!core.BaseError.someErrorName(err, core.NotFoundError.CLASS_NAME)) {
929
- throw err;
1476
+ catch (err) {
1477
+ if (!core.BaseError.someErrorName(err, core.NotFoundError.CLASS_NAME)) {
1478
+ throw err;
1479
+ }
930
1480
  }
1481
+ await this._logging?.log({
1482
+ level: "info",
1483
+ source: this.CLASS_NAME,
1484
+ message: "verifiableSyncPointerStoreNotFound",
1485
+ data: {
1486
+ key: this._synchronisedStorageKey
1487
+ }
1488
+ });
931
1489
  }
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
1490
  // If no sync pointer store exists, we return an empty one
941
1491
  return {
1492
+ version: SYNC_POINTER_STORE_VERSION,
942
1493
  syncPointers: {}
943
1494
  };
944
1495
  }
@@ -948,7 +1499,7 @@ class RemoteSyncStateHelper {
948
1499
  * @returns Nothing.
949
1500
  */
950
1501
  async storeVerifiableSyncPointerStore(syncPointerStore) {
951
- if (this._nodeIdentity) {
1502
+ if (core.Is.stringValue(this._nodeIdentity) && core.Is.stringValue(this._synchronisedStorageKey)) {
952
1503
  await this._logging?.log({
953
1504
  level: "info",
954
1505
  source: this.CLASS_NAME,
@@ -970,39 +1521,55 @@ class RemoteSyncStateHelper {
970
1521
  await this._logging?.log({
971
1522
  level: "info",
972
1523
  source: this.CLASS_NAME,
973
- message: "remoteSyncStateStoring",
1524
+ message: "syncStateStoring",
974
1525
  data: {
975
1526
  snapshotCount: syncState.snapshots.length
976
1527
  }
977
1528
  });
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);
1529
+ // Limits the number of consolidations in the list so that we can shrink decentralised
1530
+ // storage requirements, sort from newest to oldest so that we can easily find the
1531
+ // oldest snapshots to remove.
1532
+ const snapshots = syncState.snapshots.sort((a, b) => new Date(a.dateCreated).getTime() - new Date(b.dateCreated).getTime());
1533
+ // Find all the consolidation indexes
1534
+ const consolidationIndexes = [];
1535
+ for (let i = 0; i < snapshots.length; i++) {
1536
+ const snapshot = snapshots[i];
1537
+ if (snapshot.isConsolidated) {
1538
+ consolidationIndexes.push(i);
1539
+ }
1540
+ }
1541
+ if (consolidationIndexes.length > this._maxConsolidations) {
1542
+ // Once we have reached the max for consolidations we need to remove
1543
+ // all the snapshots, including non consolidated ones, beyond this point
1544
+ const toRemove = snapshots.slice(consolidationIndexes[this._maxConsolidations - 1] + 1);
1545
+ syncState.snapshots = snapshots.slice(0, consolidationIndexes[this._maxConsolidations - 1] + 1);
1546
+ for (const snapshot of toRemove) {
1547
+ await this._blobStorageHelper.removeBlob(snapshot.id);
1548
+ }
1549
+ }
1550
+ return this._blobStorageHelper.saveBlob(syncState);
981
1551
  }
982
1552
  /**
983
1553
  * Get the remote sync state.
984
1554
  * @param syncPointerId The id of the sync pointer to retrieve the state for.
985
1555
  * @returns The remote sync state.
986
1556
  */
987
- async getRemoteSyncState(syncPointerId) {
1557
+ async getSyncState(syncPointerId) {
988
1558
  try {
989
1559
  await this._logging?.log({
990
1560
  level: "info",
991
1561
  source: this.CLASS_NAME,
992
- message: "remoteSyncStateRetrieving",
1562
+ message: "syncStateRetrieving",
993
1563
  data: {
994
1564
  syncPointerId
995
1565
  }
996
1566
  });
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));
1567
+ const syncState = await this._blobStorageHelper.loadBlob(syncPointerId);
1568
+ if (core.Is.object(syncState)) {
1002
1569
  await this._logging?.log({
1003
1570
  level: "info",
1004
1571
  source: this.CLASS_NAME,
1005
- message: "remoteSyncStateRetrieved",
1572
+ message: "syncStateRetrieved",
1006
1573
  data: {
1007
1574
  syncPointerId,
1008
1575
  snapshotCount: syncState.snapshots.length
@@ -1011,33 +1578,41 @@ class RemoteSyncStateHelper {
1011
1578
  return syncState;
1012
1579
  }
1013
1580
  }
1014
- catch (err) {
1015
- if (!core.BaseError.someErrorName(err, core.NotFoundError.CLASS_NAME)) {
1016
- throw err;
1017
- }
1581
+ catch (error) {
1582
+ await this._logging?.log({
1583
+ level: "warn",
1584
+ source: this.CLASS_NAME,
1585
+ message: "getSyncStateError",
1586
+ data: {
1587
+ syncPointerId
1588
+ },
1589
+ error: core.BaseError.fromError(error)
1590
+ });
1018
1591
  }
1019
1592
  await this._logging?.log({
1020
1593
  level: "info",
1021
1594
  source: this.CLASS_NAME,
1022
- message: "remoteSyncStateNotFound",
1595
+ message: "syncStateNotFound",
1023
1596
  data: {
1024
1597
  syncPointerId
1025
1598
  }
1026
1599
  });
1027
1600
  }
1028
1601
  /**
1029
- * Handle the batch response.
1602
+ * Handle the batch response which is triggered from a consolidation request.
1030
1603
  * @param response The batch response to handle.
1031
1604
  */
1032
1605
  async handleBatchResponse(response) {
1033
1606
  if (core.Is.stringValue(this._nodeIdentity)) {
1607
+ const now = new Date(Date.now()).toISOString();
1034
1608
  // Create a new snapshot entry for the current batch
1035
1609
  const syncChangeSet = {
1036
1610
  id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
1037
- dateCreated: new Date(Date.now()).toISOString(),
1611
+ dateCreated: now,
1612
+ dateModified: now,
1038
1613
  changes: response.entities.map(change => ({
1039
1614
  operation: synchronisedStorageModels.SyncChangeOperation.Set,
1040
- id: change[response.primaryKey]
1615
+ id: change.id
1041
1616
  })),
1042
1617
  storageKey: response.storageKey,
1043
1618
  nodeIdentity: this._nodeIdentity
@@ -1045,25 +1620,42 @@ class RemoteSyncStateHelper {
1045
1620
  // And sign it with the node identity
1046
1621
  syncChangeSet.proof = await this._changeSetHelper.createChangeSetProof(syncChangeSet);
1047
1622
  // Store the changeset in the blob storage
1048
- const changeSetStorageId = await this._changeSetHelper.storeChangeSet(syncChangeSet, this._nodeIdentity);
1623
+ const changeSetStorageId = await this._changeSetHelper.storeChangeSet(syncChangeSet);
1049
1624
  // Add the changeset storage id to the snapshot ids
1050
1625
  this._batchResponseStorageIds[response.storageKey] ??= [];
1051
1626
  this._batchResponseStorageIds[response.storageKey].push(changeSetStorageId);
1627
+ // If this is the last entry in the batch response, we can create the consolidated snapshot
1052
1628
  if (response.lastEntry) {
1053
- const syncState = { snapshots: [] };
1629
+ // Get the current sync pointer store
1630
+ const syncPointerStore = await this.getVerifiableSyncPointerStore();
1631
+ let syncState;
1632
+ if (core.Is.stringValue(syncPointerStore.syncPointers[response.storageKey])) {
1633
+ // If the sync pointer exists, we load the current sync state
1634
+ syncState = await this.getSyncState(syncPointerStore.syncPointers[response.storageKey]);
1635
+ }
1636
+ // If the sync state does not exist, we create a new one
1637
+ syncState ??= {
1638
+ version: SYNC_STATE_VERSION,
1639
+ storageKey: response.storageKey,
1640
+ snapshots: []
1641
+ };
1054
1642
  const batchSnapshot = {
1643
+ version: SYNC_SNAPSHOT_VERSION,
1055
1644
  id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
1056
- dateCreated: new Date(Date.now()).toISOString(),
1645
+ dateCreated: now,
1646
+ dateModified: now,
1647
+ isConsolidated: true,
1057
1648
  changeSetStorageIds: this._batchResponseStorageIds[response.storageKey]
1058
1649
  };
1059
1650
  syncState.snapshots.push(batchSnapshot);
1060
- // Store the sync state in the blob storage
1651
+ // Store the updated sync state
1061
1652
  const syncStateId = await this.storeRemoteSyncState(syncState);
1062
- // Get the current sync pointer store
1063
- const syncPointerStore = await this.getVerifiableSyncPointerStore();
1064
1653
  syncPointerStore.syncPointers[response.storageKey] = syncStateId;
1065
1654
  // Store the verifiable sync pointer in the verifiable storage
1066
1655
  await this.storeVerifiableSyncPointerStore(syncPointerStore);
1656
+ // Remove the batch response storage ids for the storage key
1657
+ // as we have consolidated the changes
1658
+ delete this._batchResponseStorageIds[response.storageKey];
1067
1659
  await this._logging?.log({
1068
1660
  level: "info",
1069
1661
  source: this.CLASS_NAME,
@@ -1086,11 +1678,14 @@ class RemoteSyncStateHelper {
1086
1678
  id: response.id
1087
1679
  }
1088
1680
  });
1681
+ // We have received a response to an item request, find the right storage
1682
+ // for the request id
1089
1683
  if (!core.Is.empty(this._populateFullChanges[response.storageKey])) {
1090
1684
  const idx = this._populateFullChanges[response.storageKey].requestIds.indexOf(response.id);
1091
1685
  if (idx !== -1) {
1092
1686
  this._populateFullChanges[response.storageKey].requestIds.splice(idx, 1);
1093
1687
  this._populateFullChanges[response.storageKey].entities[response.id] = response.entity;
1688
+ // If there are no request ids remaining we can complete the population
1094
1689
  if (this._populateFullChanges[response.storageKey].requestIds.length === 0) {
1095
1690
  await this._populateFullChanges[response.storageKey].completeCallback();
1096
1691
  }
@@ -1118,6 +1713,11 @@ class SynchronisedStorageService {
1118
1713
  * @internal
1119
1714
  */
1120
1715
  static _DEFAULT_CONSOLIDATION_BATCH_SIZE = 100;
1716
+ /**
1717
+ * The default max number of consolidations to keep in storage.
1718
+ * @internal
1719
+ */
1720
+ static _DEFAULT_MAX_CONSOLIDATIONS = 5;
1121
1721
  /**
1122
1722
  * Runtime name for the class.
1123
1723
  */
@@ -1132,16 +1732,21 @@ class SynchronisedStorageService {
1132
1732
  * @internal
1133
1733
  */
1134
1734
  _eventBusComponent;
1735
+ /**
1736
+ * The vault connector.
1737
+ * @internal
1738
+ */
1739
+ _vaultConnector;
1135
1740
  /**
1136
1741
  * The storage connector for the sync snapshot entries.
1137
1742
  * @internal
1138
1743
  */
1139
1744
  _localSyncSnapshotEntryEntityStorage;
1140
1745
  /**
1141
- * The blob storage component to use for remote sync states.
1746
+ * The blob storage connector to use for remote sync states.
1142
1747
  * @internal
1143
1748
  */
1144
- _blobStorageComponent;
1749
+ _blobStorageConnector;
1145
1750
  /**
1146
1751
  * The verifiable storage connector to use for storing sync pointers.
1147
1752
  * @internal
@@ -1162,6 +1767,11 @@ class SynchronisedStorageService {
1162
1767
  * @internal
1163
1768
  */
1164
1769
  _trustedSynchronisedStorageComponent;
1770
+ /**
1771
+ * The blob storage helper.
1772
+ * @internal
1773
+ */
1774
+ _blobStorageHelper;
1165
1775
  /**
1166
1776
  * The change set helper.
1167
1777
  * @internal
@@ -1182,6 +1792,11 @@ class SynchronisedStorageService {
1182
1792
  * @internal
1183
1793
  */
1184
1794
  _config;
1795
+ /**
1796
+ * The synchronised storage key to use for the remote synchronised storage.
1797
+ * @internal
1798
+ */
1799
+ _synchronisedStorageKey;
1185
1800
  /**
1186
1801
  * The flag to determine if the service has been started.
1187
1802
  * @internal
@@ -1206,13 +1821,13 @@ class SynchronisedStorageService {
1206
1821
  core.Guards.object(this.CLASS_NAME, "options.config", options.config);
1207
1822
  this._eventBusComponent = core.ComponentFactory.get(options.eventBusComponentType ?? "event-bus");
1208
1823
  this._logging = loggingModels.LoggingConnectorFactory.getIfExists(options.loggingConnectorType ?? "logging");
1824
+ this._vaultConnector = vaultModels.VaultConnectorFactory.get(options.vaultConnectorType ?? "vault");
1209
1825
  this._localSyncSnapshotEntryEntityStorage = entityStorageModels.EntityStorageConnectorFactory.get(options.syncSnapshotStorageConnectorType ?? "sync-snapshot-entry");
1210
1826
  this._verifiableSyncPointerStorageConnector = verifiableStorageModels.VerifiableStorageConnectorFactory.get(options.verifiableStorageConnectorType ?? "verifiable-storage");
1211
- this._blobStorageComponent = core.ComponentFactory.get(options.blobStorageComponentType ?? "blob-storage");
1827
+ this._blobStorageConnector = blobStorageModels.BlobStorageConnectorFactory.get(options.blobStorageConnectorType ?? "blob-storage");
1212
1828
  this._identityConnector = identityModels.IdentityConnectorFactory.get(options.identityConnectorType ?? "identity");
1213
1829
  this._taskSchedulerComponent = core.ComponentFactory.get(options.taskSchedulerComponentType ?? "task-scheduler");
1214
1830
  this._config = {
1215
- synchronisedStorageKey: options.config.synchronisedStorageKey,
1216
1831
  synchronisedStorageMethodId: options.config.synchronisedStorageMethodId ?? "synchronised-storage-assertion",
1217
1832
  entityUpdateIntervalMinutes: options.config.entityUpdateIntervalMinutes ??
1218
1833
  SynchronisedStorageService._DEFAULT_ENTITY_UPDATE_INTERVAL_MINUTES,
@@ -1220,8 +1835,13 @@ class SynchronisedStorageService {
1220
1835
  consolidationIntervalMinutes: options.config.consolidationIntervalMinutes ??
1221
1836
  SynchronisedStorageService._DEFAULT_CONSOLIDATION_INTERVAL_MINUTES,
1222
1837
  consolidationBatchSize: options.config.consolidationBatchSize ??
1223
- SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE
1838
+ SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE,
1839
+ maxConsolidations: options.config.maxConsolidations ?? SynchronisedStorageService._DEFAULT_MAX_CONSOLIDATIONS,
1840
+ blobStorageEncryptionKeyId: options.config.blobStorageEncryptionKeyId ?? "synchronised-storage-blob-encryption-key",
1841
+ verifiableStorageKeyId: options.config.verifiableStorageKeyId
1224
1842
  };
1843
+ this._synchronisedStorageKey =
1844
+ verifiableStorageKeys[options.config.verifiableStorageKeyId] ?? options.config.verifiableStorageKeyId;
1225
1845
  // If this is not a trusted node, we need to use a synchronised storage service
1226
1846
  // to synchronise with a trusted node.
1227
1847
  if (!this._config.isTrustedNode) {
@@ -1229,13 +1849,19 @@ class SynchronisedStorageService {
1229
1849
  this._trustedSynchronisedStorageComponent =
1230
1850
  core.ComponentFactory.get(options.trustedSynchronisedStorageComponentType);
1231
1851
  }
1232
- this._changeSetHelper = new ChangeSetHelper(this._logging, this._eventBusComponent, this._blobStorageComponent, this._identityConnector, this._config.synchronisedStorageMethodId);
1852
+ this._blobStorageHelper = new BlobStorageHelper(this._logging, this._vaultConnector, this._blobStorageConnector, this._config.blobStorageEncryptionKeyId, this._config.isTrustedNode);
1853
+ this._changeSetHelper = new ChangeSetHelper(this._logging, this._eventBusComponent, this._identityConnector, this._blobStorageHelper, this._config.synchronisedStorageMethodId);
1233
1854
  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);
1855
+ this._remoteSyncStateHelper = new RemoteSyncStateHelper(this._logging, this._eventBusComponent, this._verifiableSyncPointerStorageConnector, this._blobStorageHelper, this._changeSetHelper, this._config.isTrustedNode, this._config.maxConsolidations);
1235
1856
  this._serviceStarted = false;
1236
1857
  this._activeStorageKeys = {};
1237
- this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.RegisterStorageKey, async (event) => this.registerType(event.data));
1238
- this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.LocalItemChange, async (event) => this._localSyncStateHelper.addLocalChange(event.data.storageKey, event.data.operation, event.data.id));
1858
+ this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.RegisterStorageKey, async (event) => this.registerStorageKey(event.data));
1859
+ this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.LocalItemChange, async (event) => {
1860
+ // Make sure the change event is from this node
1861
+ if (core.Is.stringValue(this._nodeIdentity) && this._nodeIdentity === event.data.nodeIdentity) {
1862
+ await this._localSyncStateHelper.addLocalChange(event.data.storageKey, event.data.operation, event.data.id);
1863
+ }
1864
+ });
1239
1865
  }
1240
1866
  /**
1241
1867
  * The component needs to be started when the node is initialized.
@@ -1247,7 +1873,16 @@ class SynchronisedStorageService {
1247
1873
  async start(nodeIdentity, nodeLoggingConnectorType, componentState) {
1248
1874
  this._nodeIdentity = nodeIdentity;
1249
1875
  this._remoteSyncStateHelper.setNodeIdentity(nodeIdentity);
1876
+ this._changeSetHelper.setNodeIdentity(nodeIdentity);
1877
+ this._remoteSyncStateHelper.setSynchronisedStorageKey(this._synchronisedStorageKey);
1250
1878
  this._serviceStarted = true;
1879
+ // If this is not a trusted node we need to request the decryption key from a trusted node
1880
+ if (!this._config.isTrustedNode && !core.Is.empty(this._trustedSynchronisedStorageComponent)) {
1881
+ const proof = await this._identityConnector.createProof(this._nodeIdentity, identityModels.DocumentHelper.joinId(this._nodeIdentity, this._config.synchronisedStorageMethodId), standardsW3cDid.ProofTypes.DataIntegrityProof, { nodeIdentity });
1882
+ const decryptionKey = await this._trustedSynchronisedStorageComponent.getDecryptionKey(this._nodeIdentity, proof);
1883
+ // We don't have the private key so instead we store the key as a secret in the vault
1884
+ await this._vaultConnector.setSecret(this._config.blobStorageEncryptionKeyId, decryptionKey);
1885
+ }
1251
1886
  // If there are already storage keys registered, we need to activate them
1252
1887
  for (const storageKey in this._activeStorageKeys) {
1253
1888
  await this.activateStorageKey(storageKey);
@@ -1268,24 +1903,59 @@ class SynchronisedStorageService {
1268
1903
  }
1269
1904
  }
1270
1905
  /**
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.
1906
+ * Get the decryption key for the synchronised storage.
1907
+ * This is used to decrypt the data stored in the synchronised storage.
1908
+ * @param nodeIdentity The identity of the node requesting the decryption key.
1909
+ * @param proof The proof of the request so we know the request is from the specified node.
1910
+ * @returns The decryption key.
1911
+ */
1912
+ async getDecryptionKey(nodeIdentity, proof) {
1913
+ if (!this._config.isTrustedNode) {
1914
+ throw new core.GeneralError(this.CLASS_NAME, "notTrustedNode");
1915
+ }
1916
+ core.Guards.stringValue(this.CLASS_NAME, "nodeIdentity", nodeIdentity);
1917
+ core.Guards.object(this.CLASS_NAME, "proof", proof);
1918
+ const isValid = await this._identityConnector.verifyProof({ nodeIdentity }, proof);
1919
+ if (!isValid) {
1920
+ throw new core.UnauthorizedError(this.CLASS_NAME, "invalidProof");
1921
+ }
1922
+ // TODO: We need to check if the node has permissions to access the decryption key
1923
+ // using rights-management
1924
+ const key = await this._vaultConnector.getKey(this._config.blobStorageEncryptionKeyId);
1925
+ if (core.Is.undefined(key.publicKey)) {
1926
+ throw new core.UnauthorizedError(this.CLASS_NAME, "decryptionKeyNotFound");
1927
+ }
1928
+ return core.Converter.bytesToBase64(key.publicKey);
1929
+ }
1930
+ /**
1931
+ * Synchronise a set of changes from an untrusted node, assumes this is a trusted node.
1932
+ * @param syncChangeSet The change set to synchronise.
1273
1933
  * @returns Nothing.
1274
1934
  */
1275
- async syncChangeSet(changeSetStorageId) {
1935
+ async syncChangeSet(syncChangeSet) {
1276
1936
  if (!this._config.isTrustedNode) {
1277
1937
  throw new core.GeneralError(this.CLASS_NAME, "notTrustedNode");
1278
1938
  }
1279
- // This method is called by non trusted nodes to synchronise changes
1280
- core.Guards.stringValue(this.CLASS_NAME, "changeSetStorageId", changeSetStorageId);
1939
+ core.Guards.object(this.CLASS_NAME, "syncChangeSet", syncChangeSet);
1940
+ await this._logging?.log({
1941
+ level: "info",
1942
+ source: this.CLASS_NAME,
1943
+ message: "syncChangeSetForRemoteNode",
1944
+ data: {
1945
+ changeSetStorageId: syncChangeSet.id
1946
+ }
1947
+ });
1281
1948
  // TODO: The change set has a proof signed by the originating node identity
1282
1949
  // The proof is verified that the change set is valid and has not been tampered with.
1283
1950
  // but we also need to check that the originating node has permissions
1284
1951
  // to store the change set in the synchronised storage.
1285
1952
  // 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);
1953
+ const copy = await this._changeSetHelper.copyChangeset(syncChangeSet);
1954
+ if (!core.Is.empty(copy)) {
1955
+ // Apply the changes to this node
1956
+ await this._changeSetHelper.applyChangeset(copy.syncChangeSet);
1957
+ // And update the sync state with the latest changes
1958
+ await this._remoteSyncStateHelper.addChangeSetToSyncState(copy.syncChangeSet.storageKey, copy.changeSetStorageId);
1289
1959
  }
1290
1960
  }
1291
1961
  /**
@@ -1338,10 +2008,10 @@ class SynchronisedStorageService {
1338
2008
  if (!core.Is.empty(verifiableSyncPointerStore.syncPointers[storageKey])) {
1339
2009
  // Load the sync state from the remote blob storage using the sync pointer
1340
2010
  // to load the sync state
1341
- const remoteSyncState = await this._remoteSyncStateHelper.getRemoteSyncState(verifiableSyncPointerStore.syncPointers[storageKey]);
2011
+ const remoteSyncState = await this._remoteSyncStateHelper.getSyncState(verifiableSyncPointerStore.syncPointers[storageKey]);
1342
2012
  // If we got the sync state we can try and sync from it
1343
2013
  if (!core.Is.undefined(remoteSyncState)) {
1344
- await this._localSyncStateHelper.syncFromRemote(storageKey, remoteSyncState);
2014
+ await this._localSyncStateHelper.applySyncState(storageKey, remoteSyncState);
1345
2015
  }
1346
2016
  }
1347
2017
  }
@@ -1359,51 +2029,67 @@ class SynchronisedStorageService {
1359
2029
  storageKey
1360
2030
  }
1361
2031
  });
1362
- const localChangeSnapshot = await this._localSyncStateHelper.getLocalChangeSnapshot(storageKey);
1363
- if (core.Is.arrayValue(localChangeSnapshot.changes)) {
1364
- await this._remoteSyncStateHelper.createAndStoreChangeSet(storageKey, localChangeSnapshot.changes, async (changeSetStorageId) => {
1365
- if (core.Is.stringValue(changeSetStorageId)) {
1366
- await this._logging?.log({
1367
- level: "info",
1368
- source: this.CLASS_NAME,
1369
- message: "createdStorageChangeSet",
1370
- data: {
1371
- storageKey,
1372
- changeSetStorageId
2032
+ const localChangeSnapshots = await this._localSyncStateHelper.getSnapshots(storageKey, true);
2033
+ if (localChangeSnapshots.length > 0) {
2034
+ const localChangeSnapshot = localChangeSnapshots[0];
2035
+ if (core.Is.arrayValue(localChangeSnapshot.changes)) {
2036
+ await this._remoteSyncStateHelper.buildChangeSet(storageKey, localChangeSnapshot.changes, async (syncChangeSet, changeSetStorageId) => {
2037
+ if (core.Is.empty(syncChangeSet) && core.Is.empty(changeSetStorageId)) {
2038
+ await this._logging?.log({
2039
+ level: "info",
2040
+ source: this.CLASS_NAME,
2041
+ message: "builtStorageChangeSetNone",
2042
+ data: {
2043
+ storageKey
2044
+ }
2045
+ });
2046
+ }
2047
+ else {
2048
+ await this._logging?.log({
2049
+ level: "info",
2050
+ source: this.CLASS_NAME,
2051
+ message: "builtStorageChangeSet",
2052
+ data: {
2053
+ storageKey,
2054
+ changeSetStorageId
2055
+ }
2056
+ });
2057
+ // Send the local changes to the remote storage if we are a trusted node
2058
+ if (this._config.isTrustedNode && core.Is.stringValue(changeSetStorageId)) {
2059
+ // If we are a trusted node, we can add the change set to the sync state
2060
+ // and remove the local change snapshot
2061
+ await this._remoteSyncStateHelper.addChangeSetToSyncState(storageKey, changeSetStorageId);
2062
+ await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
2063
+ }
2064
+ else if (!core.Is.empty(this._trustedSynchronisedStorageComponent) &&
2065
+ core.Is.object(syncChangeSet)) {
2066
+ // If we are not a trusted node, we need to send the changes to the trusted node
2067
+ // and then remove the local change snapshot
2068
+ await this._logging?.log({
2069
+ level: "info",
2070
+ source: this.CLASS_NAME,
2071
+ message: "sendingChangeSetToTrustedNode",
2072
+ data: {
2073
+ storageKey,
2074
+ changeSetStorageId
2075
+ }
2076
+ });
2077
+ await this._trustedSynchronisedStorageComponent.syncChangeSet(syncChangeSet);
2078
+ await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
1373
2079
  }
1374
- });
1375
- // Send the local changes to the remote storage if we are a trusted node
1376
- if (this._config.isTrustedNode) {
1377
- await this._remoteSyncStateHelper.addChangeSetToSyncState(storageKey, changeSetStorageId);
1378
- await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
1379
2080
  }
1380
- else if (!core.Is.empty(this._trustedSynchronisedStorageComponent)) {
1381
- // If we are not a trusted node, we need to send the changes to the trusted node
1382
- await this._trustedSynchronisedStorageComponent.syncChangeSet(changeSetStorageId);
1383
- await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
2081
+ });
2082
+ }
2083
+ else {
2084
+ await this._logging?.log({
2085
+ level: "info",
2086
+ source: this.CLASS_NAME,
2087
+ message: "updateFromLocalSyncStateNoChanges",
2088
+ data: {
2089
+ storageKey
1384
2090
  }
1385
- }
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
- });
1397
- }
1398
- else {
1399
- await this._logging?.log({
1400
- level: "info",
1401
- source: this.CLASS_NAME,
1402
- message: "updateFromLocalSyncStateNoChanges",
1403
- data: {
1404
- storageKey
1405
- }
1406
- });
2091
+ });
2092
+ }
1407
2093
  }
1408
2094
  }
1409
2095
  /**
@@ -1415,17 +2101,17 @@ class SynchronisedStorageService {
1415
2101
  async startConsolidationSync(storageKey) {
1416
2102
  let localChangeSnapshot;
1417
2103
  try {
1418
- // If we are performing a consolidation, we can remove the local changes
1419
- await this._localSyncStateHelper.getLocalChangeSnapshot(storageKey);
2104
+ // If we are performing a consolidation, we can remove the local change snapshot
2105
+ // as we are going to create a complete changeset from the DB
2106
+ const localChangeSnapshots = await this._localSyncStateHelper.getSnapshots(storageKey, true);
2107
+ localChangeSnapshot = localChangeSnapshots[0];
1420
2108
  if (!core.Is.empty(localChangeSnapshot)) {
1421
2109
  await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
1422
2110
  }
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
- }
2111
+ await this._remoteSyncStateHelper.consolidationStart(storageKey, this._config.consolidationBatchSize ??
2112
+ SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE);
2113
+ // The consolidation was successful, so we can remove the local change snapshot permanently
2114
+ localChangeSnapshot = undefined;
1429
2115
  }
1430
2116
  catch (error) {
1431
2117
  if (localChangeSnapshot) {
@@ -1442,22 +2128,22 @@ class SynchronisedStorageService {
1442
2128
  }
1443
2129
  /**
1444
2130
  * Register a new sync type.
1445
- * @param syncRegisterType The sync register type to register.
2131
+ * @param syncRegisterStorageKey The sync register type to register.
1446
2132
  * @internal
1447
2133
  */
1448
- async registerType(syncRegisterType) {
2134
+ async registerStorageKey(syncRegisterStorageKey) {
1449
2135
  await this._logging?.log({
1450
2136
  level: "info",
1451
2137
  source: this.CLASS_NAME,
1452
- message: "registerType",
2138
+ message: "registerStorageKey",
1453
2139
  data: {
1454
- storageKey: syncRegisterType.storageKey
2140
+ storageKey: syncRegisterStorageKey.storageKey
1455
2141
  }
1456
2142
  });
1457
- if (core.Is.empty(this._activeStorageKeys[syncRegisterType.storageKey])) {
1458
- this._activeStorageKeys[syncRegisterType.storageKey] = false;
2143
+ if (core.Is.empty(this._activeStorageKeys[syncRegisterStorageKey.storageKey])) {
2144
+ this._activeStorageKeys[syncRegisterStorageKey.storageKey] = false;
1459
2145
  if (this._serviceStarted) {
1460
- await this.activateStorageKey(syncRegisterType.storageKey);
2146
+ await this.activateStorageKey(syncRegisterStorageKey.storageKey);
1461
2147
  }
1462
2148
  }
1463
2149
  }
@@ -1471,7 +2157,7 @@ class SynchronisedStorageService {
1471
2157
  await this._logging?.log({
1472
2158
  level: "info",
1473
2159
  source: this.CLASS_NAME,
1474
- message: "activateType",
2160
+ message: "activateStorageKey",
1475
2161
  data: {
1476
2162
  storageKey
1477
2163
  }
@@ -1501,5 +2187,6 @@ exports.SynchronisedStorageService = SynchronisedStorageService;
1501
2187
  exports.generateRestRoutesSynchronisedStorage = generateRestRoutesSynchronisedStorage;
1502
2188
  exports.initSchema = initSchema;
1503
2189
  exports.restEntryPoints = restEntryPoints;
2190
+ exports.synchronisedStorageGetDecryptionKeyRequest = synchronisedStorageGetDecryptionKeyRequest;
1504
2191
  exports.synchronisedStorageSyncChangeSetRequest = synchronisedStorageSyncChangeSetRequest;
1505
2192
  exports.tagsSynchronisedStorage = tagsSynchronisedStorage;