@twin.org/node-core 0.0.2-next.3 → 0.0.2-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.
@@ -3,6 +3,7 @@
3
3
  var apiAuthEntityStorageService = require('@twin.org/api-auth-entity-storage-service');
4
4
  var core = require('@twin.org/core');
5
5
  var crypto = require('@twin.org/crypto');
6
+ var engineServerTypes = require('@twin.org/engine-server-types');
6
7
  var engineTypes = require('@twin.org/engine-types');
7
8
  var entityStorageModels = require('@twin.org/entity-storage-models');
8
9
  var identityModels = require('@twin.org/identity-models');
@@ -11,7 +12,6 @@ var walletModels = require('@twin.org/wallet-models');
11
12
  var promises = require('node:fs/promises');
12
13
  var path = require('node:path');
13
14
  var engineServer = require('@twin.org/engine-server');
14
- var engineServerTypes = require('@twin.org/engine-server-types');
15
15
  var dotenv = require('dotenv');
16
16
  var engine = require('@twin.org/engine');
17
17
  var engineCore = require('@twin.org/engine-core');
@@ -50,7 +50,11 @@ const NodeFeatures = {
50
50
  /**
51
51
  * NodeUser - generates a user for the node if not provided in config.
52
52
  */
53
- NodeUser: "node-user"
53
+ NodeUser: "node-user",
54
+ /**
55
+ * NodeWallet - generates a wallet for the node and funds it when there is a faucet available.
56
+ */
57
+ NodeWallet: "node-wallet"
54
58
  };
55
59
 
56
60
  // Copyright 2024 IOTA Stiftung.
@@ -137,8 +141,9 @@ async function bootstrap(engineCore, context, envVars) {
137
141
  await bootstrapNodeUser(engineCore, context, envVars, features);
138
142
  await bootstrapAuth(engineCore, context, envVars);
139
143
  await bootstrapBlobEncryption(engineCore, context, envVars);
140
- await bootstrapAttestationMethod(engineCore, context, envVars);
141
- await bootstrapImmutableProofMethod(engineCore, context, envVars);
144
+ await addVerificationMethod(engineCore, context, "attestation", envVars.attestationVerificationMethodId);
145
+ await addVerificationMethod(engineCore, context, "immutable proof", envVars.immutableProofVerificationMethodId);
146
+ await bootstrapSynchronisedStorage(engineCore, context, envVars);
142
147
  }
143
148
  /**
144
149
  * Bootstrap the node creating any necessary resources.
@@ -153,23 +158,21 @@ async function bootstrapNodeIdentity(engineCore, context, envVars, features) {
153
158
  // But we have a chicken and egg problem in that we can't create the identity
154
159
  // to store the mnemonic in the vault without an identity. We use a temporary identity
155
160
  // and then replace it with the new identity later in the process.
156
- const engineDefaultTypes = engineCore.getDefaultTypes();
157
- if (!core.Is.empty(engineDefaultTypes.vaultConnector)) {
158
- const vaultConnector = vaultModels.VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
159
- const workingIdentity = envVars.identity ??
160
- context.state.nodeIdentity ??
161
- `bootstrap-temp-${core.Converter.bytesToHex(core.RandomHelper.generate(16))}`;
162
- await bootstrapMnemonic(engineCore, envVars, features, vaultConnector, workingIdentity);
163
- const addresses = await bootstrapWallet(engineCore, envVars, features, workingIdentity);
164
- const finalIdentity = await bootstrapIdentity(engineCore, envVars, features, workingIdentity);
165
- await finaliseWallet(engineCore, envVars, features, finalIdentity, addresses);
166
- await finaliseMnemonic(vaultConnector, workingIdentity, finalIdentity);
167
- context.state.nodeIdentity = finalIdentity;
168
- context.stateDirty = true;
169
- engineCore.logInfo(core.I18n.formatMessage("node.nodeIdentity", {
170
- identity: context.state.nodeIdentity
171
- }));
172
- }
161
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
162
+ const vaultConnector = vaultModels.VaultConnectorFactory.get(defaultVaultConnectorType);
163
+ const workingIdentity = envVars.identity ??
164
+ context.state.nodeIdentity ??
165
+ `bootstrap-temp-${core.Converter.bytesToHex(core.RandomHelper.generate(16))}`;
166
+ await bootstrapMnemonic(engineCore, envVars, features, vaultConnector, workingIdentity);
167
+ const addresses = await bootstrapWallet(engineCore, envVars, features, workingIdentity);
168
+ const finalIdentity = await bootstrapIdentity(engineCore, envVars, features, workingIdentity);
169
+ await finaliseWallet(engineCore, envVars, features, finalIdentity, addresses);
170
+ await finaliseMnemonic(vaultConnector, workingIdentity, finalIdentity);
171
+ context.state.nodeIdentity = finalIdentity;
172
+ context.stateDirty = true;
173
+ engineCore.logInfo(core.I18n.formatMessage("node.nodeIdentity", {
174
+ identity: context.state.nodeIdentity
175
+ }));
173
176
  }
174
177
  }
175
178
  /**
@@ -181,12 +184,13 @@ async function bootstrapNodeIdentity(engineCore, context, envVars, features) {
181
184
  * @returns The addresses for the wallet.
182
185
  */
183
186
  async function bootstrapIdentity(engineCore, envVars, features, nodeIdentity) {
184
- const engineDefaultTypes = engineCore.getDefaultTypes();
187
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
185
188
  // Now create an identity for the node controlled by the address we just funded
186
- const identityConnector = identityModels.IdentityConnectorFactory.get(engineDefaultTypes.identityConnector);
189
+ const identityConnector = identityModels.IdentityConnectorFactory.get(defaultIdentityConnectorType);
187
190
  let identityDocument;
188
191
  try {
189
- const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(engineDefaultTypes.identityResolverConnector);
192
+ const defaultIdentityResolverConnectorType = engineCore.getRegisteredInstanceType("identityResolverConnector");
193
+ const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(defaultIdentityResolverConnectorType);
190
194
  identityDocument = await identityResolverConnector.resolveDocument(nodeIdentity);
191
195
  engineCore.logInfo(core.I18n.formatMessage("node.existingNodeIdentity", { identity: nodeIdentity }));
192
196
  }
@@ -196,7 +200,7 @@ async function bootstrapIdentity(engineCore, envVars, features, nodeIdentity) {
196
200
  identityDocument = await identityConnector.createDocument(nodeIdentity);
197
201
  engineCore.logInfo(core.I18n.formatMessage("node.createdNodeIdentity", { identity: identityDocument.id }));
198
202
  }
199
- if (engineDefaultTypes.identityConnector === engineTypes.IdentityConnectorType.Iota) {
203
+ if (defaultIdentityConnectorType.startsWith(engineTypes.IdentityConnectorType.Iota)) {
200
204
  const didUrn = core.Urn.fromValidString(identityDocument.id);
201
205
  const didParts = didUrn.parts();
202
206
  const objectId = didParts[3];
@@ -215,23 +219,26 @@ async function bootstrapIdentity(engineCore, envVars, features, nodeIdentity) {
215
219
  * @returns The addresses for the wallet.
216
220
  */
217
221
  async function bootstrapWallet(engineCore, envVars, features, nodeIdentity) {
218
- const engineDefaultTypes = engineCore.getDefaultTypes();
219
- const walletConnector = walletModels.WalletConnectorFactory.get(engineDefaultTypes.walletConnector);
220
- const addresses = await walletConnector.getAddresses(nodeIdentity, 0, 0, 5);
221
- const balance = await walletConnector.getBalance(nodeIdentity, addresses[0]);
222
- if (balance === 0n) {
223
- let address0 = addresses[0];
224
- if (engineDefaultTypes.walletConnector === engineTypes.WalletConnectorType.Iota) {
225
- address0 = `${envVars.iotaExplorerEndpoint}address/${address0}?network=${envVars.iotaNetwork}`;
222
+ if (features.includes(NodeFeatures.NodeWallet)) {
223
+ const defaultWalletConnectorType = engineCore.getRegisteredInstanceType("walletConnector");
224
+ const walletConnector = walletModels.WalletConnectorFactory.get(defaultWalletConnectorType);
225
+ const addresses = await walletConnector.getAddresses(nodeIdentity, 0, 0, 5);
226
+ const balance = await walletConnector.getBalance(nodeIdentity, addresses[0]);
227
+ if (balance === 0n) {
228
+ let address0 = addresses[0];
229
+ if (defaultWalletConnectorType.startsWith(engineTypes.WalletConnectorType.Iota)) {
230
+ address0 = `${envVars.iotaExplorerEndpoint}address/${address0}?network=${envVars.iotaNetwork}`;
231
+ }
232
+ engineCore.logInfo(core.I18n.formatMessage("node.fundingWallet", { address: address0 }));
233
+ // Add some funds to the wallet from the faucet
234
+ await walletConnector.ensureBalance(nodeIdentity, addresses[0], 1000000000n);
226
235
  }
227
- engineCore.logInfo(core.I18n.formatMessage("node.fundingWallet", { address: address0 }));
228
- // Add some funds to the wallet from the faucet
229
- await walletConnector.ensureBalance(nodeIdentity, addresses[0], 1000000000n);
230
- }
231
- else {
232
- engineCore.logInfo(core.I18n.formatMessage("node.fundedWallet"));
236
+ else {
237
+ engineCore.logInfo(core.I18n.formatMessage("node.fundedWallet"));
238
+ }
239
+ return addresses;
233
240
  }
234
- return addresses;
241
+ return [];
235
242
  }
236
243
  /**
237
244
  * Bootstrap the identity for the node.
@@ -242,15 +249,17 @@ async function bootstrapWallet(engineCore, envVars, features, nodeIdentity) {
242
249
  * @param addresses The addresses for the wallet.
243
250
  */
244
251
  async function finaliseWallet(engineCore, envVars, features, finalIdentity, addresses) {
245
- const engineDefaultTypes = engineCore.getDefaultTypes();
246
- // If we are using entity storage for wallet the identity associated with the
247
- // address will be wrong, so fix it
248
- if (engineDefaultTypes.walletConnector === engineTypes.WalletConnectorType.EntityStorage) {
249
- const walletAddress = entityStorageModels.EntityStorageConnectorFactory.get(core.StringHelper.kebabCase("WalletAddress"));
250
- const addr = await walletAddress.get(addresses[0]);
251
- if (!core.Is.empty(addr)) {
252
- addr.identity = finalIdentity;
253
- await walletAddress.set(addr);
252
+ if (features.includes(NodeFeatures.NodeWallet)) {
253
+ const defaultWalletConnectorType = engineCore.getRegisteredInstanceType("walletConnector");
254
+ // If we are using entity storage for wallet the identity associated with the
255
+ // address will be wrong, so fix it
256
+ if (defaultWalletConnectorType.startsWith(engineTypes.WalletConnectorType.EntityStorage)) {
257
+ const walletAddress = entityStorageModels.EntityStorageConnectorFactory.get(core.StringHelper.kebabCase("WalletAddress"));
258
+ const addr = await walletAddress.get(addresses[0]);
259
+ if (!core.Is.empty(addr)) {
260
+ addr.identity = finalIdentity;
261
+ await walletAddress.set(addr);
262
+ }
254
263
  }
255
264
  }
256
265
  }
@@ -312,8 +321,8 @@ async function finaliseMnemonic(vaultConnector, workingIdentity, finalIdentity)
312
321
  */
313
322
  async function bootstrapNodeUser(engineCore, context, envVars, features) {
314
323
  if (features.includes(NodeFeatures.NodeUser)) {
315
- const engineDefaultTypes = engineCore.getDefaultTypes();
316
- if (engineDefaultTypes.authenticationComponent === "entity-storage-authentication-service" &&
324
+ const defaultAuthenticationComponentType = engineCore.getRegisteredInstanceType("authenticationComponent");
325
+ if (defaultAuthenticationComponentType.startsWith(engineServerTypes.AuthenticationComponentType.EntityStorage) &&
317
326
  core.Is.stringValue(context.state.nodeIdentity)) {
318
327
  const authUserEntityStorage = entityStorageModels.EntityStorageConnectorFactory.get(core.StringHelper.kebabCase("AuthenticationUser"));
319
328
  const email = envVars.username ?? DEFAULT_NODE_USERNAME;
@@ -356,7 +365,8 @@ async function bootstrapNodeUser(engineCore, context, envVars, features) {
356
365
  }
357
366
  }
358
367
  // We have create a node user, now we need to create a profile for the user
359
- const identityProfileConnector = identityModels.IdentityProfileConnectorFactory.get(engineDefaultTypes.identityProfileConnector);
368
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
369
+ const identityProfileConnector = identityModels.IdentityProfileConnectorFactory.get(defaultIdentityConnectorType);
360
370
  if (identityProfileConnector) {
361
371
  let userProfile;
362
372
  try {
@@ -386,43 +396,6 @@ async function bootstrapNodeUser(engineCore, context, envVars, features) {
386
396
  }
387
397
  }
388
398
  }
389
- /**
390
- * Bootstrap the attestation verification methods.
391
- * @param engineCore The engine core for the node.
392
- * @param context The context for the node.
393
- * @param envVars The environment variables for the node.
394
- * @param features The features that are enabled on the node.
395
- */
396
- async function bootstrapAttestationMethod(engineCore, context, envVars, features) {
397
- if (core.Is.stringValue(context.state.nodeIdentity) &&
398
- core.Is.arrayValue(context.config.types.identityConnector) &&
399
- core.Is.stringValue(envVars.attestationVerificationMethodId)) {
400
- const engineDefaultTypes = engineCore.getDefaultTypes();
401
- const identityConnector = identityModels.IdentityConnectorFactory.get(engineDefaultTypes.identityConnector);
402
- const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(engineDefaultTypes.identityResolverConnector);
403
- const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
404
- const fullMethodId = `${identityDocument.id}#${envVars.attestationVerificationMethodId}`;
405
- let createVm = true;
406
- try {
407
- identityModels.DocumentHelper.getVerificationMethod(identityDocument, fullMethodId, "assertionMethod");
408
- createVm = false;
409
- }
410
- catch { }
411
- if (createVm) {
412
- // Add attestation verification method to DID, the correct node context is now in place
413
- // so the keys for the verification method will be stored correctly
414
- engineCore.logInfo(core.I18n.formatMessage("node.addingAttestation", {
415
- methodId: fullMethodId
416
- }));
417
- await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", envVars.attestationVerificationMethodId);
418
- }
419
- else {
420
- engineCore.logInfo(core.I18n.formatMessage("node.existingAttestation", {
421
- methodId: fullMethodId
422
- }));
423
- }
424
- }
425
- }
426
399
  /**
427
400
  * Bootstrap the immutable proof verification methods.
428
401
  * @param engineCore The engine core for the node.
@@ -430,36 +403,7 @@ async function bootstrapAttestationMethod(engineCore, context, envVars, features
430
403
  * @param envVars The environment variables for the node.
431
404
  * @param features The features that are enabled on the node.
432
405
  */
433
- async function bootstrapImmutableProofMethod(engineCore, context, envVars, features) {
434
- const engineDefaultTypes = engineCore.getDefaultTypes();
435
- if (core.Is.stringValue(context.state.nodeIdentity) &&
436
- core.Is.arrayValue(context.config.types.identityConnector) &&
437
- core.Is.stringValue(envVars.immutableProofVerificationMethodId)) {
438
- const identityConnector = identityModels.IdentityConnectorFactory.get(engineDefaultTypes.identityConnector);
439
- const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(engineDefaultTypes.identityResolverConnector);
440
- const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
441
- const fullMethodId = `${identityDocument.id}#${envVars.immutableProofVerificationMethodId}`;
442
- let createVm = true;
443
- try {
444
- identityModels.DocumentHelper.getVerificationMethod(identityDocument, fullMethodId, "assertionMethod");
445
- createVm = false;
446
- }
447
- catch { }
448
- if (createVm) {
449
- // Add AIG verification method to DID, the correct node context is now in place
450
- // so the keys for the verification method will be stored correctly
451
- engineCore.logInfo(core.I18n.formatMessage("node.addingImmutableProof", {
452
- methodId: fullMethodId
453
- }));
454
- await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", envVars.immutableProofVerificationMethodId);
455
- }
456
- else {
457
- engineCore.logInfo(core.I18n.formatMessage("node.existingImmutableProof", {
458
- methodId: fullMethodId
459
- }));
460
- }
461
- }
462
- }
406
+ async function bootstrapImmutableProofMethod(engineCore, context, envVars, features) { }
463
407
  /**
464
408
  * Bootstrap the keys for blob encryption.
465
409
  * @param engineCore The engine core for the node.
@@ -470,18 +414,28 @@ async function bootstrapImmutableProofMethod(engineCore, context, envVars, featu
470
414
  async function bootstrapBlobEncryption(engineCore, context, envVars, features) {
471
415
  if ((core.Coerce.boolean(envVars.blobStorageEnableEncryption) ?? false) &&
472
416
  core.Is.stringValue(context.state.nodeIdentity)) {
473
- const engineDefaultTypes = engineCore.getDefaultTypes();
474
417
  // Create a new key for encrypting blobs
475
- const vaultConnector = vaultModels.VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
476
- const keyName = `${context.state.nodeIdentity}/${envVars.blobStorageEncryptionKey}`;
418
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
419
+ const vaultConnector = vaultModels.VaultConnectorFactory.get(defaultVaultConnectorType);
420
+ const keyName = `${context.state.nodeIdentity}/${envVars.blobStorageEncryptionKeyId}`;
477
421
  let existingKey;
478
422
  try {
479
423
  existingKey = await vaultConnector.getKey(keyName);
480
424
  }
481
425
  catch { }
482
426
  if (core.Is.empty(existingKey)) {
483
- engineCore.logInfo(core.I18n.formatMessage("node.creatingBlobEncryptionKey", { keyName }));
484
- await vaultConnector.createKey(keyName, vaultModels.VaultKeyType.ChaCha20Poly1305);
427
+ if (core.Is.stringBase64(envVars.blobStorageSymmetricEncryptionKey)) {
428
+ engineCore.logInfo(core.I18n.formatMessage("node.addingBlobEncryptionKey", { keyName }));
429
+ await vaultConnector.addKey(keyName, vaultModels.VaultKeyType.ChaCha20Poly1305, core.Converter.base64ToBytes(envVars.blobStorageSymmetricEncryptionKey));
430
+ }
431
+ else {
432
+ engineCore.logInfo(core.I18n.formatMessage("node.creatingBlobEncryptionKey", { keyName }));
433
+ const key = await vaultConnector.createKey(keyName, vaultModels.VaultKeyType.ChaCha20Poly1305);
434
+ engineCore.logInfo(core.I18n.formatMessage("node.createdBlobEncryptionKey", {
435
+ keyName,
436
+ keyValue: core.Converter.bytesToBase64(key)
437
+ }));
438
+ }
485
439
  }
486
440
  else {
487
441
  engineCore.logInfo(core.I18n.formatMessage("node.existingBlobEncryptionKey", { keyName }));
@@ -496,11 +450,13 @@ async function bootstrapBlobEncryption(engineCore, context, envVars, features) {
496
450
  * @param features The features that are enabled on the node.
497
451
  */
498
452
  async function bootstrapAuth(engineCore, context, envVars, features) {
499
- const engineDefaultTypes = engineCore.getDefaultTypes();
500
- if (engineDefaultTypes.authenticationComponent === "entity-storage-authentication-service" &&
453
+ const defaultAuthenticationComponentType = engineCore.getRegisteredInstanceTypeOptional("authenticationComponent");
454
+ if (core.Is.stringValue(defaultAuthenticationComponentType) &&
455
+ defaultAuthenticationComponentType.startsWith(engineServerTypes.AuthenticationComponentType.EntityStorage) &&
501
456
  core.Is.stringValue(context.state.nodeIdentity)) {
502
457
  // Create a new JWT signing key and a user login for the node
503
- const vaultConnector = vaultModels.VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
458
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
459
+ const vaultConnector = vaultModels.VaultConnectorFactory.get(defaultVaultConnectorType);
504
460
  const keyName = `${context.state.nodeIdentity}/${envVars.authSigningKeyId}`;
505
461
  let existingKey;
506
462
  try {
@@ -516,6 +472,77 @@ async function bootstrapAuth(engineCore, context, envVars, features) {
516
472
  }
517
473
  }
518
474
  }
475
+ /**
476
+ * Bootstrap the synchronised storage blob encryption and verification methods.
477
+ * @param engineCore The engine core for the node.
478
+ * @param context The context for the node.
479
+ * @param envVars The environment variables for the node.
480
+ * @param features The features that are enabled on the node.
481
+ */
482
+ async function bootstrapSynchronisedStorage(engineCore, context, envVars, features) {
483
+ if (core.Coerce.boolean(envVars.synchronisedStorageEnabled) ?? false) {
484
+ // Add the verification method to the identity if it doesn't exist
485
+ await addVerificationMethod(engineCore, context, "synchronised storage", envVars.synchronisedStorageVerificationMethodId);
486
+ // If this is a trusted node we need to add the blob encryption key pair
487
+ if (core.Is.stringValue(envVars.synchronisedStorageBlobStorageEncryptionKeyId) &&
488
+ core.Is.stringBase64(envVars.synchronisedStorageBlobStoragePrivateKey) &&
489
+ core.Is.stringBase64(envVars.synchronisedStorageBlobStoragePublicKey)) {
490
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
491
+ const vaultConnector = vaultModels.VaultConnectorFactory.get(defaultVaultConnectorType);
492
+ const keyName = envVars.synchronisedStorageBlobStorageEncryptionKeyId;
493
+ let existingKey;
494
+ try {
495
+ existingKey = await vaultConnector.getKey(keyName);
496
+ }
497
+ catch { }
498
+ if (core.Is.empty(existingKey)) {
499
+ engineCore.logInfo(core.I18n.formatMessage("node.addingSynchronisedStorageBlobEncryptionKey", { keyName }));
500
+ await vaultConnector.addKey(keyName, vaultModels.VaultKeyType.Rsa2048, core.Converter.base64ToBytes(envVars.synchronisedStorageBlobStoragePrivateKey), core.Converter.base64ToBytes(envVars.synchronisedStorageBlobStoragePublicKey));
501
+ }
502
+ else {
503
+ engineCore.logInfo(core.I18n.formatMessage("node.existingSynchronisedStorageBlobEncryptionKey", { keyName }));
504
+ }
505
+ }
506
+ }
507
+ }
508
+ /**
509
+ * Add a verification method if it doesn't exist.
510
+ * @param engineCore The engine core for the node.
511
+ * @param context The context for the node.
512
+ * @param verificationMethodTitle The verification method title.
513
+ * @param verificationMethodId The verification method ID.
514
+ */
515
+ async function addVerificationMethod(engineCore, context, verificationMethodTitle, verificationMethodId) {
516
+ if (core.Is.stringValue(context.state.nodeIdentity) &&
517
+ core.Is.arrayValue(context.config.types.identityConnector) &&
518
+ core.Is.stringValue(verificationMethodId)) {
519
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
520
+ const identityConnector = identityModels.IdentityConnectorFactory.get(defaultIdentityConnectorType);
521
+ const defaultIdentityResolverConnectorType = engineCore.getRegisteredInstanceType("identityResolverConnector");
522
+ const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(defaultIdentityResolverConnectorType);
523
+ const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
524
+ const fullMethodId = `${identityDocument.id}#${verificationMethodId}`;
525
+ let exists = false;
526
+ try {
527
+ identityModels.DocumentHelper.getVerificationMethod(identityDocument, fullMethodId, "assertionMethod");
528
+ exists = true;
529
+ }
530
+ catch { }
531
+ if (!exists) {
532
+ engineCore.logInfo(core.I18n.formatMessage("node.addingVerificationMethod", {
533
+ title: verificationMethodTitle,
534
+ methodId: fullMethodId
535
+ }));
536
+ await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", verificationMethodId);
537
+ }
538
+ else {
539
+ engineCore.logInfo(core.I18n.formatMessage("node.existingVerificationMethod", {
540
+ title: verificationMethodTitle,
541
+ methodId: fullMethodId
542
+ }));
543
+ }
544
+ }
545
+ }
519
546
 
520
547
  // Copyright 2024 IOTA Stiftung.
521
548
  // SPDX-License-Identifier: Apache-2.0.
@@ -533,7 +560,9 @@ function buildEngineConfiguration(envVars) {
533
560
  envVars.attestationVerificationMethodId ??= "attestation-assertion";
534
561
  envVars.immutableProofVerificationMethodId ??= "immutable-proof-assertion";
535
562
  envVars.blobStorageEnableEncryption ??= "false";
536
- envVars.blobStorageEncryptionKey ??= "blob-encryption";
563
+ envVars.blobStorageEncryptionKeyId ??= "blob-encryption";
564
+ envVars.synchronisedStorageBlobStorageEncryptionKeyId ??= "synchronised-storage-blob-encryption";
565
+ envVars.synchronisedStorageVerificationMethodId ??= "synchronised-storage-assertion";
537
566
  const coreConfig = {
538
567
  debug: core.Coerce.boolean(envVars.debug) ?? false,
539
568
  types: {}
@@ -559,9 +588,10 @@ function buildEngineConfiguration(envVars) {
559
588
  configureAuditableItemGraph(coreConfig);
560
589
  configureAuditableItemStream(coreConfig);
561
590
  configureDocumentManagement(coreConfig);
562
- configureFederatedCatalogue(coreConfig, envVars);
563
591
  configureRightsManagement(coreConfig, envVars);
564
592
  configureTaskScheduler(coreConfig, envVars);
593
+ configureSynchronisedStorage(coreConfig, envVars);
594
+ configureFederatedCatalogue(coreConfig, envVars);
565
595
  return coreConfig;
566
596
  }
567
597
  /**
@@ -683,7 +713,7 @@ function configureEntityStorage(coreConfig, envVars) {
683
713
  }
684
714
  });
685
715
  }
686
- if (entityStorageConnectorTypes) {
716
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.PostgreSql)) {
687
717
  coreConfig.types.entityStorageConnector.push({
688
718
  type: engineTypes.EntityStorageConnectorType.PostgreSql,
689
719
  options: {
@@ -698,10 +728,19 @@ function configureEntityStorage(coreConfig, envVars) {
698
728
  }
699
729
  });
700
730
  }
731
+ const defaultEntityStorageConnectorType = envVars.entityStorageConnectorDefault ?? entityStorageConnectorTypes[0];
732
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.Synchronised)) {
733
+ // For synchronised storage we use the default connector as the one we wrap for real DB operations
734
+ coreConfig.types.entityStorageConnector.push({
735
+ type: engineTypes.EntityStorageConnectorType.Synchronised,
736
+ options: {
737
+ entityStorageConnectorType: defaultEntityStorageConnectorType
738
+ }
739
+ });
740
+ }
701
741
  if (core.Is.arrayValue(entityStorageConnectorTypes)) {
702
- const defaultStorageConnectorType = envVars.entityStorageConnectorDefault ?? entityStorageConnectorTypes[0];
703
742
  for (const config of coreConfig.types.entityStorageConnector) {
704
- if (config.type === defaultStorageConnectorType) {
743
+ if (config.type === defaultEntityStorageConnectorType) {
705
744
  config.isDefault = true;
706
745
  break;
707
746
  }
@@ -793,6 +832,13 @@ function configureBlobStorage(coreConfig, envVars) {
793
832
  for (const config of coreConfig.types.blobStorageConnector) {
794
833
  if (config.type === defaultStorageConnectorType) {
795
834
  config.isDefault = true;
835
+ }
836
+ // If this blob storage connector is the one to use for public access
837
+ // then add it as a feature
838
+ if (core.Is.stringValue(envVars.blobStorageConnectorPublic) &&
839
+ config.type === envVars.blobStorageConnectorPublic) {
840
+ config.features ??= [];
841
+ config.features.push("public");
796
842
  break;
797
843
  }
798
844
  }
@@ -804,7 +850,7 @@ function configureBlobStorage(coreConfig, envVars) {
804
850
  options: {
805
851
  config: {
806
852
  vaultKeyId: (envVars.blobStorageEnableEncryption ?? false)
807
- ? envVars.blobStorageEncryptionKey
853
+ ? envVars.blobStorageEncryptionKeyId
808
854
  : undefined
809
855
  }
810
856
  }
@@ -1298,25 +1344,6 @@ function configureDocumentManagement(coreConfig, envVars) {
1298
1344
  });
1299
1345
  }
1300
1346
  }
1301
- /**
1302
- * Configures the federated catalogue.
1303
- * @param coreConfig The core config.
1304
- * @param envVars The environment variables.
1305
- */
1306
- function configureFederatedCatalogue(coreConfig, envVars) {
1307
- if (core.Is.arrayValue(coreConfig.types.identityResolverComponent)) {
1308
- coreConfig.types.federatedCatalogueComponent ??= [];
1309
- coreConfig.types.federatedCatalogueComponent.push({
1310
- type: engineTypes.FederatedCatalogueComponentType.Service,
1311
- options: {
1312
- config: {
1313
- subResourceCacheTtlMs: core.Coerce.number(envVars.federatedCatalogueCacheTtlMs),
1314
- clearingHouseApproverList: core.Coerce.object(envVars.federatedCatalogueClearingHouseApproverList) ?? []
1315
- }
1316
- }
1317
- });
1318
- }
1319
- }
1320
1347
  /**
1321
1348
  * Configures the rights management.
1322
1349
  * @param coreConfig The core config.
@@ -1340,13 +1367,76 @@ function configureRightsManagement(coreConfig, envVars) {
1340
1367
  * @param envVars The environment variables.
1341
1368
  */
1342
1369
  function configureTaskScheduler(coreConfig, envVars) {
1343
- if (core.Coerce.boolean(envVars.taskSchedulerEnabled) ?? true) {
1370
+ if (core.Coerce.boolean(envVars.taskSchedulerEnabled) ?? false) {
1344
1371
  coreConfig.types.taskSchedulerComponent ??= [];
1345
1372
  coreConfig.types.taskSchedulerComponent.push({
1346
1373
  type: engineTypes.TaskSchedulerComponentType.Service
1347
1374
  });
1348
1375
  }
1349
1376
  }
1377
+ /**
1378
+ * Configures the synchronised storage.
1379
+ * @param coreConfig The core config.
1380
+ * @param envVars The environment variables.
1381
+ */
1382
+ function configureSynchronisedStorage(coreConfig, envVars) {
1383
+ if (core.Is.arrayValue(coreConfig.types.identityResolverComponent) &&
1384
+ (core.Coerce.boolean(envVars.synchronisedStorageEnabled) ?? false)) {
1385
+ // Check if the config provides a custom verifiable storage key id
1386
+ let verifiableStorageKeyId = core.Coerce.string(envVars.synchronisedStorageVerifiableStorageKeyId);
1387
+ if (!core.Is.stringValue(verifiableStorageKeyId)) {
1388
+ // No custom key so default to the network setting
1389
+ verifiableStorageKeyId = envVars.iotaNetwork;
1390
+ }
1391
+ coreConfig.types.synchronisedStorageComponent ??= [];
1392
+ coreConfig.types.synchronisedStorageComponent.push({
1393
+ type: engineTypes.SynchronisedStorageComponentType.Service,
1394
+ options: {
1395
+ config: {
1396
+ verifiableStorageKeyId: verifiableStorageKeyId ?? "",
1397
+ synchronisedStorageMethodId: envVars.synchronisedStorageVerificationMethodId,
1398
+ blobStorageEncryptionKeyId: envVars.synchronisedStorageBlobStorageEncryptionKeyId,
1399
+ entityUpdateIntervalMinutes: core.Coerce.number(envVars.synchronisedStorageEntityUpdateIntervalMinutes),
1400
+ consolidationIntervalMinutes: core.Coerce.number(envVars.synchronisedStorageConsolidationIntervalMinutes),
1401
+ consolidationBatchSize: core.Coerce.number(envVars.synchronisedStorageConsolidationBatchSize),
1402
+ maxConsolidations: core.Coerce.number(envVars.synchronisedStorageMaxConsolidations)
1403
+ }
1404
+ }
1405
+ });
1406
+ // If there is a trusted url set, we need to add a client
1407
+ // and give it a feature of trusted so that when the synchronised
1408
+ // storage is created it can pickup the correct component
1409
+ if (core.Is.stringValue(envVars.synchronisedStorageTrustedUrl)) {
1410
+ coreConfig.types.synchronisedStorageComponent.push({
1411
+ type: engineTypes.SynchronisedStorageComponentType.RestClient,
1412
+ options: {
1413
+ endpoint: envVars.synchronisedStorageTrustedUrl
1414
+ },
1415
+ features: ["trusted"]
1416
+ });
1417
+ }
1418
+ }
1419
+ }
1420
+ /**
1421
+ * Configures the federated catalogue.
1422
+ * @param coreConfig The core config.
1423
+ * @param envVars The environment variables.
1424
+ */
1425
+ function configureFederatedCatalogue(coreConfig, envVars) {
1426
+ if (core.Is.arrayValue(coreConfig.types.identityResolverComponent) &&
1427
+ (core.Coerce.boolean(envVars.federatedCatalogueEnabled) ?? false)) {
1428
+ coreConfig.types.federatedCatalogueComponent ??= [];
1429
+ coreConfig.types.federatedCatalogueComponent.push({
1430
+ type: engineTypes.FederatedCatalogueComponentType.Service,
1431
+ options: {
1432
+ config: {
1433
+ subResourceCacheTtlMs: core.Coerce.number(envVars.federatedCatalogueCacheTtlMs),
1434
+ clearingHouseApproverList: core.Coerce.object(envVars.federatedCatalogueClearingHouseApproverList) ?? []
1435
+ }
1436
+ }
1437
+ });
1438
+ }
1439
+ }
1350
1440
  /**
1351
1441
  * Configures the DLT.
1352
1442
  * @param coreConfig The core config.
@@ -1532,9 +1622,10 @@ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, o
1532
1622
  * @returns The engine server.
1533
1623
  */
1534
1624
  async function start(nodeOptions, engineServerConfig, envVars) {
1535
- envVars.storageFileRoot ??= "";
1536
1625
  const entityStorageConnectorType = envVars.entityStorageConnectorType?.split(",") ?? [];
1537
1626
  const blobStorageConnectorType = envVars.blobStorageConnectorType?.split(",") ?? [];
1627
+ // If the blob storage or entity storage is configured with file connectors
1628
+ // then we need to make sure the storageFileRoot is set
1538
1629
  if ((entityStorageConnectorType.includes(engineTypes.EntityStorageConnectorType.File) ||
1539
1630
  blobStorageConnectorType.includes(engineTypes.BlobStorageConnectorType.File) ||
1540
1631
  core.Is.empty(nodeOptions?.stateStorage)) &&
@@ -1543,7 +1634,7 @@ async function start(nodeOptions, engineServerConfig, envVars) {
1543
1634
  storageFileRoot: `${nodeOptions?.envPrefix ?? ""}_STORAGE_FILE_ROOT`
1544
1635
  });
1545
1636
  }
1546
- // Create the engine instance using file state storage and custom bootstrap.
1637
+ // Create the engine instance using file state storage unless one is configured in options
1547
1638
  const engine$1 = new engine.Engine({
1548
1639
  config: engineServerConfig,
1549
1640
  stateStorage: nodeOptions?.stateStorage ?? new engineCore.FileStateStorage(envVars.stateFilename ?? ""),
@@ -1587,7 +1678,7 @@ async function run(nodeOptions) {
1587
1678
  nodeOptions ??= {};
1588
1679
  const serverInfo = {
1589
1680
  name: nodeOptions?.serverName ?? "TWIN Node Server",
1590
- version: nodeOptions?.serverVersion ?? "0.0.2-next.3" // x-release-please-version
1681
+ version: nodeOptions?.serverVersion ?? "0.0.2-next.4" // x-release-please-version
1591
1682
  };
1592
1683
  console.log(`\u001B[4mđŸŒŠī¸ ${serverInfo.name} v${serverInfo.version}\u001B[24m\n`);
1593
1684
  if (!core.Is.stringValue(nodeOptions?.executionDirectory)) {
@@ -1645,7 +1736,8 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1645
1736
  }
1646
1737
  if (core.Is.arrayValue(options?.envFilenames)) {
1647
1738
  const output = dotenv__namespace.config({
1648
- path: options?.envFilenames
1739
+ path: options?.envFilenames,
1740
+ quiet: true
1649
1741
  });
1650
1742
  // We don't want to throw an error if the default environment file is not found.
1651
1743
  // Only if we have custom environment files.
@@ -1699,12 +1791,12 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1699
1791
 
1700
1792
  exports.NodeFeatures = NodeFeatures;
1701
1793
  exports.bootstrap = bootstrap;
1702
- exports.bootstrapAttestationMethod = bootstrapAttestationMethod;
1703
1794
  exports.bootstrapAuth = bootstrapAuth;
1704
1795
  exports.bootstrapBlobEncryption = bootstrapBlobEncryption;
1705
1796
  exports.bootstrapImmutableProofMethod = bootstrapImmutableProofMethod;
1706
1797
  exports.bootstrapNodeIdentity = bootstrapNodeIdentity;
1707
1798
  exports.bootstrapNodeUser = bootstrapNodeUser;
1799
+ exports.bootstrapSynchronisedStorage = bootstrapSynchronisedStorage;
1708
1800
  exports.buildConfiguration = buildConfiguration;
1709
1801
  exports.buildEngineConfiguration = buildEngineConfiguration;
1710
1802
  exports.buildEngineServerConfiguration = buildEngineServerConfiguration;