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