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