@twin.org/node-core 0.0.2-next.3 → 0.0.2-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,76 @@ 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.synchronisedStorageBlobStorageKey)) {
489
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
490
+ const vaultConnector = vaultModels.VaultConnectorFactory.get(defaultVaultConnectorType);
491
+ const keyName = envVars.synchronisedStorageBlobStorageEncryptionKeyId;
492
+ let existingKey;
493
+ try {
494
+ existingKey = await vaultConnector.getKey(keyName);
495
+ }
496
+ catch { }
497
+ if (core.Is.empty(existingKey)) {
498
+ engineCore.logInfo(core.I18n.formatMessage("node.addingSynchronisedStorageBlobEncryptionKey", { keyName }));
499
+ await vaultConnector.addKey(keyName, vaultModels.VaultKeyType.ChaCha20Poly1305, core.Converter.base64ToBytes(envVars.synchronisedStorageBlobStorageKey));
500
+ }
501
+ else {
502
+ engineCore.logInfo(core.I18n.formatMessage("node.existingSynchronisedStorageBlobEncryptionKey", { keyName }));
503
+ }
504
+ }
505
+ }
506
+ }
507
+ /**
508
+ * Add a verification method if it doesn't exist.
509
+ * @param engineCore The engine core for the node.
510
+ * @param context The context for the node.
511
+ * @param verificationMethodTitle The verification method title.
512
+ * @param verificationMethodId The verification method ID.
513
+ */
514
+ async function addVerificationMethod(engineCore, context, verificationMethodTitle, verificationMethodId) {
515
+ if (core.Is.stringValue(context.state.nodeIdentity) &&
516
+ core.Is.arrayValue(context.config.types.identityConnector) &&
517
+ core.Is.stringValue(verificationMethodId)) {
518
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
519
+ const identityConnector = identityModels.IdentityConnectorFactory.get(defaultIdentityConnectorType);
520
+ const defaultIdentityResolverConnectorType = engineCore.getRegisteredInstanceType("identityResolverConnector");
521
+ const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(defaultIdentityResolverConnectorType);
522
+ const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
523
+ const fullMethodId = `${identityDocument.id}#${verificationMethodId}`;
524
+ let exists = false;
525
+ try {
526
+ identityModels.DocumentHelper.getVerificationMethod(identityDocument, fullMethodId, "assertionMethod");
527
+ exists = true;
528
+ }
529
+ catch { }
530
+ if (!exists) {
531
+ engineCore.logInfo(core.I18n.formatMessage("node.addingVerificationMethod", {
532
+ title: verificationMethodTitle,
533
+ methodId: fullMethodId
534
+ }));
535
+ await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", verificationMethodId);
536
+ }
537
+ else {
538
+ engineCore.logInfo(core.I18n.formatMessage("node.existingVerificationMethod", {
539
+ title: verificationMethodTitle,
540
+ methodId: fullMethodId
541
+ }));
542
+ }
543
+ }
544
+ }
519
545
 
520
546
  // Copyright 2024 IOTA Stiftung.
521
547
  // SPDX-License-Identifier: Apache-2.0.
@@ -533,7 +559,9 @@ function buildEngineConfiguration(envVars) {
533
559
  envVars.attestationVerificationMethodId ??= "attestation-assertion";
534
560
  envVars.immutableProofVerificationMethodId ??= "immutable-proof-assertion";
535
561
  envVars.blobStorageEnableEncryption ??= "false";
536
- envVars.blobStorageEncryptionKey ??= "blob-encryption";
562
+ envVars.blobStorageEncryptionKeyId ??= "blob-encryption";
563
+ envVars.synchronisedStorageBlobStorageEncryptionKeyId ??= "synchronised-storage-blob-encryption";
564
+ envVars.synchronisedStorageVerificationMethodId ??= "synchronised-storage-assertion";
537
565
  const coreConfig = {
538
566
  debug: core.Coerce.boolean(envVars.debug) ?? false,
539
567
  types: {}
@@ -559,9 +587,10 @@ function buildEngineConfiguration(envVars) {
559
587
  configureAuditableItemGraph(coreConfig);
560
588
  configureAuditableItemStream(coreConfig);
561
589
  configureDocumentManagement(coreConfig);
562
- configureFederatedCatalogue(coreConfig, envVars);
563
590
  configureRightsManagement(coreConfig, envVars);
564
591
  configureTaskScheduler(coreConfig, envVars);
592
+ configureSynchronisedStorage(coreConfig, envVars);
593
+ configureFederatedCatalogue(coreConfig, envVars);
565
594
  return coreConfig;
566
595
  }
567
596
  /**
@@ -683,7 +712,7 @@ function configureEntityStorage(coreConfig, envVars) {
683
712
  }
684
713
  });
685
714
  }
686
- if (entityStorageConnectorTypes) {
715
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.PostgreSql)) {
687
716
  coreConfig.types.entityStorageConnector.push({
688
717
  type: engineTypes.EntityStorageConnectorType.PostgreSql,
689
718
  options: {
@@ -698,10 +727,19 @@ function configureEntityStorage(coreConfig, envVars) {
698
727
  }
699
728
  });
700
729
  }
730
+ const defaultEntityStorageConnectorType = envVars.entityStorageConnectorDefault ?? entityStorageConnectorTypes[0];
731
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.Synchronised)) {
732
+ // For synchronised storage we use the default connector as the one we wrap for real DB operations
733
+ coreConfig.types.entityStorageConnector.push({
734
+ type: engineTypes.EntityStorageConnectorType.Synchronised,
735
+ options: {
736
+ entityStorageConnectorType: defaultEntityStorageConnectorType
737
+ }
738
+ });
739
+ }
701
740
  if (core.Is.arrayValue(entityStorageConnectorTypes)) {
702
- const defaultStorageConnectorType = envVars.entityStorageConnectorDefault ?? entityStorageConnectorTypes[0];
703
741
  for (const config of coreConfig.types.entityStorageConnector) {
704
- if (config.type === defaultStorageConnectorType) {
742
+ if (config.type === defaultEntityStorageConnectorType) {
705
743
  config.isDefault = true;
706
744
  break;
707
745
  }
@@ -793,6 +831,13 @@ function configureBlobStorage(coreConfig, envVars) {
793
831
  for (const config of coreConfig.types.blobStorageConnector) {
794
832
  if (config.type === defaultStorageConnectorType) {
795
833
  config.isDefault = true;
834
+ }
835
+ // If this blob storage connector is the one to use for public access
836
+ // then add it as a feature
837
+ if (core.Is.stringValue(envVars.blobStorageConnectorPublic) &&
838
+ config.type === envVars.blobStorageConnectorPublic) {
839
+ config.features ??= [];
840
+ config.features.push("public");
796
841
  break;
797
842
  }
798
843
  }
@@ -804,7 +849,7 @@ function configureBlobStorage(coreConfig, envVars) {
804
849
  options: {
805
850
  config: {
806
851
  vaultKeyId: (envVars.blobStorageEnableEncryption ?? false)
807
- ? envVars.blobStorageEncryptionKey
852
+ ? envVars.blobStorageEncryptionKeyId
808
853
  : undefined
809
854
  }
810
855
  }
@@ -1298,25 +1343,6 @@ function configureDocumentManagement(coreConfig, envVars) {
1298
1343
  });
1299
1344
  }
1300
1345
  }
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
1346
  /**
1321
1347
  * Configures the rights management.
1322
1348
  * @param coreConfig The core config.
@@ -1340,13 +1366,76 @@ function configureRightsManagement(coreConfig, envVars) {
1340
1366
  * @param envVars The environment variables.
1341
1367
  */
1342
1368
  function configureTaskScheduler(coreConfig, envVars) {
1343
- if (core.Coerce.boolean(envVars.taskSchedulerEnabled) ?? true) {
1369
+ if (core.Coerce.boolean(envVars.taskSchedulerEnabled) ?? false) {
1344
1370
  coreConfig.types.taskSchedulerComponent ??= [];
1345
1371
  coreConfig.types.taskSchedulerComponent.push({
1346
1372
  type: engineTypes.TaskSchedulerComponentType.Service
1347
1373
  });
1348
1374
  }
1349
1375
  }
1376
+ /**
1377
+ * Configures the synchronised storage.
1378
+ * @param coreConfig The core config.
1379
+ * @param envVars The environment variables.
1380
+ */
1381
+ function configureSynchronisedStorage(coreConfig, envVars) {
1382
+ if (core.Is.arrayValue(coreConfig.types.identityResolverComponent) &&
1383
+ (core.Coerce.boolean(envVars.synchronisedStorageEnabled) ?? false)) {
1384
+ // Check if the config provides a custom verifiable storage key id
1385
+ let verifiableStorageKeyId = core.Coerce.string(envVars.synchronisedStorageVerifiableStorageKeyId);
1386
+ if (!core.Is.stringValue(verifiableStorageKeyId)) {
1387
+ // No custom key so default to the network setting
1388
+ verifiableStorageKeyId = envVars.iotaNetwork;
1389
+ }
1390
+ coreConfig.types.synchronisedStorageComponent ??= [];
1391
+ coreConfig.types.synchronisedStorageComponent.push({
1392
+ type: engineTypes.SynchronisedStorageComponentType.Service,
1393
+ options: {
1394
+ config: {
1395
+ verifiableStorageKeyId: verifiableStorageKeyId ?? "",
1396
+ synchronisedStorageMethodId: envVars.synchronisedStorageVerificationMethodId,
1397
+ blobStorageEncryptionKeyId: envVars.synchronisedStorageBlobStorageEncryptionKeyId,
1398
+ entityUpdateIntervalMinutes: core.Coerce.number(envVars.synchronisedStorageEntityUpdateIntervalMinutes),
1399
+ consolidationIntervalMinutes: core.Coerce.number(envVars.synchronisedStorageConsolidationIntervalMinutes),
1400
+ consolidationBatchSize: core.Coerce.number(envVars.synchronisedStorageConsolidationBatchSize),
1401
+ maxConsolidations: core.Coerce.number(envVars.synchronisedStorageMaxConsolidations)
1402
+ }
1403
+ }
1404
+ });
1405
+ // If there is a trusted url set, we need to add a client
1406
+ // and give it a feature of trusted so that when the synchronised
1407
+ // storage is created it can pickup the correct component
1408
+ if (core.Is.stringValue(envVars.synchronisedStorageTrustedUrl)) {
1409
+ coreConfig.types.synchronisedStorageComponent.push({
1410
+ type: engineTypes.SynchronisedStorageComponentType.RestClient,
1411
+ options: {
1412
+ endpoint: envVars.synchronisedStorageTrustedUrl
1413
+ },
1414
+ features: ["trusted"]
1415
+ });
1416
+ }
1417
+ }
1418
+ }
1419
+ /**
1420
+ * Configures the federated catalogue.
1421
+ * @param coreConfig The core config.
1422
+ * @param envVars The environment variables.
1423
+ */
1424
+ function configureFederatedCatalogue(coreConfig, envVars) {
1425
+ if (core.Is.arrayValue(coreConfig.types.identityResolverComponent) &&
1426
+ (core.Coerce.boolean(envVars.federatedCatalogueEnabled) ?? false)) {
1427
+ coreConfig.types.federatedCatalogueComponent ??= [];
1428
+ coreConfig.types.federatedCatalogueComponent.push({
1429
+ type: engineTypes.FederatedCatalogueComponentType.Service,
1430
+ options: {
1431
+ config: {
1432
+ subResourceCacheTtlMs: core.Coerce.number(envVars.federatedCatalogueCacheTtlMs),
1433
+ clearingHouseApproverList: core.Coerce.object(envVars.federatedCatalogueClearingHouseApproverList) ?? []
1434
+ }
1435
+ }
1436
+ });
1437
+ }
1438
+ }
1350
1439
  /**
1351
1440
  * Configures the DLT.
1352
1441
  * @param coreConfig The core config.
@@ -1386,9 +1475,10 @@ function configureDlt(coreConfig, envVars) {
1386
1475
  * @param coreEngineConfig The core engine config.
1387
1476
  * @param serverInfo The server information.
1388
1477
  * @param openApiSpecPath The path to the open api spec.
1478
+ * @param favIconPath The path to the favicon.
1389
1479
  * @returns The the config for the core and the server.
1390
1480
  */
1391
- function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, openApiSpecPath) {
1481
+ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, openApiSpecPath, favIconPath) {
1392
1482
  envVars.authSigningKeyId ??= "auth-signing";
1393
1483
  const webServerOptions = {
1394
1484
  port: core.Coerce.number(envVars.port),
@@ -1415,7 +1505,8 @@ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, o
1415
1505
  options: {
1416
1506
  config: {
1417
1507
  serverInfo,
1418
- openApiSpecPath
1508
+ openApiSpecPath,
1509
+ favIconPath
1419
1510
  }
1420
1511
  }
1421
1512
  }
@@ -1532,9 +1623,10 @@ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, o
1532
1623
  * @returns The engine server.
1533
1624
  */
1534
1625
  async function start(nodeOptions, engineServerConfig, envVars) {
1535
- envVars.storageFileRoot ??= "";
1536
1626
  const entityStorageConnectorType = envVars.entityStorageConnectorType?.split(",") ?? [];
1537
1627
  const blobStorageConnectorType = envVars.blobStorageConnectorType?.split(",") ?? [];
1628
+ // If the blob storage or entity storage is configured with file connectors
1629
+ // then we need to make sure the storageFileRoot is set
1538
1630
  if ((entityStorageConnectorType.includes(engineTypes.EntityStorageConnectorType.File) ||
1539
1631
  blobStorageConnectorType.includes(engineTypes.BlobStorageConnectorType.File) ||
1540
1632
  core.Is.empty(nodeOptions?.stateStorage)) &&
@@ -1543,7 +1635,7 @@ async function start(nodeOptions, engineServerConfig, envVars) {
1543
1635
  storageFileRoot: `${nodeOptions?.envPrefix ?? ""}_STORAGE_FILE_ROOT`
1544
1636
  });
1545
1637
  }
1546
- // Create the engine instance using file state storage and custom bootstrap.
1638
+ // Create the engine instance using file state storage unless one is configured in options
1547
1639
  const engine$1 = new engine.Engine({
1548
1640
  config: engineServerConfig,
1549
1641
  stateStorage: nodeOptions?.stateStorage ?? new engineCore.FileStateStorage(envVars.stateFilename ?? ""),
@@ -1587,7 +1679,7 @@ async function run(nodeOptions) {
1587
1679
  nodeOptions ??= {};
1588
1680
  const serverInfo = {
1589
1681
  name: nodeOptions?.serverName ?? "TWIN Node Server",
1590
- version: nodeOptions?.serverVersion ?? "0.0.2-next.3" // x-release-please-version
1682
+ version: nodeOptions?.serverVersion ?? "0.0.2-next.5" // x-release-please-version
1591
1683
  };
1592
1684
  console.log(`\u001B[4mđŸŒŠī¸ ${serverInfo.name} v${serverInfo.version}\u001B[24m\n`);
1593
1685
  if (!core.Is.stringValue(nodeOptions?.executionDirectory)) {
@@ -1600,13 +1692,21 @@ async function run(nodeOptions) {
1600
1692
  console.info("Locales Directory:", nodeOptions.localesDirectory);
1601
1693
  await initialiseLocales(nodeOptions.localesDirectory);
1602
1694
  if (core.Is.empty(nodeOptions?.openApiSpecFile)) {
1603
- const specFile = path.resolve(path.join(nodeOptions.executionDirectory, "docs", "open-api", "spec.json"));
1695
+ const specFile = path.resolve(path.join(nodeOptions.executionDirectory ?? "", "docs", "open-api", "spec.json"));
1604
1696
  console.info("Default OpenAPI Spec File:", specFile);
1605
1697
  if (await fileExists(specFile)) {
1606
1698
  nodeOptions ??= {};
1607
1699
  nodeOptions.openApiSpecFile = specFile;
1608
1700
  }
1609
1701
  }
1702
+ if (core.Is.empty(nodeOptions?.favIconFile)) {
1703
+ const favIconFile = path.resolve(path.join(nodeOptions.executionDirectory ?? "", "static", "favicon.png"));
1704
+ console.info("Default Favicon File:", favIconFile);
1705
+ if (await fileExists(favIconFile)) {
1706
+ nodeOptions ??= {};
1707
+ nodeOptions.favIconFile = favIconFile;
1708
+ }
1709
+ }
1610
1710
  nodeOptions.envPrefix ??= "TWIN_NODE_";
1611
1711
  console.info("Environment Prefix:", nodeOptions.envPrefix);
1612
1712
  const { engineServerConfig, nodeEnvVars: envVars } = await buildConfiguration(process.env, nodeOptions, serverInfo);
@@ -1645,7 +1745,8 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1645
1745
  }
1646
1746
  if (core.Is.arrayValue(options?.envFilenames)) {
1647
1747
  const output = dotenv__namespace.config({
1648
- path: options?.envFilenames
1748
+ path: options?.envFilenames,
1749
+ quiet: true
1649
1750
  });
1650
1751
  // We don't want to throw an error if the default environment file is not found.
1651
1752
  // Only if we have custom environment files.
@@ -1675,7 +1776,7 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1675
1776
  }
1676
1777
  // Build the engine configuration from the environment variables.
1677
1778
  const coreConfig = buildEngineConfiguration(envVars);
1678
- const engineServerConfig = buildEngineServerConfiguration(envVars, coreConfig, serverInfo, options?.openApiSpecFile);
1779
+ const engineServerConfig = buildEngineServerConfiguration(envVars, coreConfig, serverInfo, options?.openApiSpecFile, options?.favIconFile);
1679
1780
  // Merge any custom configuration provided in the options.
1680
1781
  if (core.Is.arrayValue(options?.configFilenames)) {
1681
1782
  for (const configFile of options.configFilenames) {
@@ -1699,12 +1800,12 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1699
1800
 
1700
1801
  exports.NodeFeatures = NodeFeatures;
1701
1802
  exports.bootstrap = bootstrap;
1702
- exports.bootstrapAttestationMethod = bootstrapAttestationMethod;
1703
1803
  exports.bootstrapAuth = bootstrapAuth;
1704
1804
  exports.bootstrapBlobEncryption = bootstrapBlobEncryption;
1705
1805
  exports.bootstrapImmutableProofMethod = bootstrapImmutableProofMethod;
1706
1806
  exports.bootstrapNodeIdentity = bootstrapNodeIdentity;
1707
1807
  exports.bootstrapNodeUser = bootstrapNodeUser;
1808
+ exports.bootstrapSynchronisedStorage = bootstrapSynchronisedStorage;
1708
1809
  exports.buildConfiguration = buildConfiguration;
1709
1810
  exports.buildEngineConfiguration = buildEngineConfiguration;
1710
1811
  exports.buildEngineServerConfiguration = buildEngineServerConfiguration;