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