@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.
@@ -1,7 +1,8 @@
1
1
  import { PasswordHelper } from '@twin.org/api-auth-entity-storage-service';
2
2
  import { I18n, Is, Converter, RandomHelper, StringHelper, Coerce, Urn, GeneralError, ErrorHelper, EnvHelper } from '@twin.org/core';
3
3
  import { PasswordGenerator, Bip39 } from '@twin.org/crypto';
4
- import { WalletConnectorType, IdentityConnectorType, EntityStorageConnectorType, BlobStorageConnectorType, BlobStorageComponentType, VaultConnectorType, DltConfigType, LoggingConnectorType, LoggingComponentType, BackgroundTaskConnectorType, EventBusConnectorType, EventBusComponentType, TelemetryConnectorType, TelemetryComponentType, MessagingEmailConnectorType, MessagingSmsConnectorType, MessagingPushNotificationConnectorType, MessagingComponentType, FaucetConnectorType, NftConnectorType, NftComponentType, VerifiableStorageConnectorType, VerifiableStorageComponentType, ImmutableProofComponentType, AuditableItemGraphComponentType, AuditableItemStreamComponentType, IdentityComponentType, IdentityResolverConnectorType, IdentityResolverComponentType, IdentityProfileConnectorType, IdentityProfileComponentType, AttestationConnectorType, AttestationComponentType, DataConverterConnectorType, DataExtractorConnectorType, DataProcessingComponentType, DocumentManagementComponentType, FederatedCatalogueComponentType, RightsManagementPapComponentType, RightsManagementComponentType, TaskSchedulerComponentType } from '@twin.org/engine-types';
4
+ import { AuthenticationComponentType, InformationComponentType, RestRouteProcessorType, SocketRouteProcessorType, AuthenticationAdminComponentType } from '@twin.org/engine-server-types';
5
+ import { WalletConnectorType, IdentityConnectorType, EntityStorageConnectorType, BlobStorageConnectorType, BlobStorageComponentType, VaultConnectorType, DltConfigType, LoggingConnectorType, LoggingComponentType, BackgroundTaskConnectorType, EventBusConnectorType, EventBusComponentType, TelemetryConnectorType, TelemetryComponentType, MessagingEmailConnectorType, MessagingSmsConnectorType, MessagingPushNotificationConnectorType, MessagingComponentType, FaucetConnectorType, NftConnectorType, NftComponentType, VerifiableStorageConnectorType, VerifiableStorageComponentType, ImmutableProofComponentType, AuditableItemGraphComponentType, AuditableItemStreamComponentType, IdentityComponentType, IdentityResolverConnectorType, IdentityResolverComponentType, IdentityProfileConnectorType, IdentityProfileComponentType, AttestationConnectorType, AttestationComponentType, DataConverterConnectorType, DataExtractorConnectorType, DataProcessingComponentType, DocumentManagementComponentType, RightsManagementPapComponentType, RightsManagementComponentType, TaskSchedulerComponentType, SynchronisedStorageComponentType, FederatedCatalogueComponentType } from '@twin.org/engine-types';
5
6
  import { EntityStorageConnectorFactory } from '@twin.org/entity-storage-models';
6
7
  import { IdentityProfileConnectorFactory, IdentityConnectorFactory, IdentityResolverConnectorFactory, DocumentHelper } from '@twin.org/identity-models';
7
8
  import { VaultConnectorFactory, VaultKeyType } from '@twin.org/vault-models';
@@ -9,7 +10,6 @@ import { WalletConnectorFactory } from '@twin.org/wallet-models';
9
10
  import { readFile, stat } from 'node:fs/promises';
10
11
  import path from 'node:path';
11
12
  import { addDefaultRestPaths, addDefaultSocketPaths, EngineServer } from '@twin.org/engine-server';
12
- import { InformationComponentType, RestRouteProcessorType, SocketRouteProcessorType, AuthenticationAdminComponentType, AuthenticationComponentType } from '@twin.org/engine-server-types';
13
13
  import * as dotenv from 'dotenv';
14
14
  import { Engine } from '@twin.org/engine';
15
15
  import { FileStateStorage } from '@twin.org/engine-core';
@@ -29,7 +29,11 @@ const NodeFeatures = {
29
29
  /**
30
30
  * NodeUser - generates a user for the node if not provided in config.
31
31
  */
32
- NodeUser: "node-user"
32
+ NodeUser: "node-user",
33
+ /**
34
+ * NodeWallet - generates a wallet for the node and funds it when there is a faucet available.
35
+ */
36
+ NodeWallet: "node-wallet"
33
37
  };
34
38
 
35
39
  // Copyright 2024 IOTA Stiftung.
@@ -116,8 +120,9 @@ async function bootstrap(engineCore, context, envVars) {
116
120
  await bootstrapNodeUser(engineCore, context, envVars, features);
117
121
  await bootstrapAuth(engineCore, context, envVars);
118
122
  await bootstrapBlobEncryption(engineCore, context, envVars);
119
- await bootstrapAttestationMethod(engineCore, context, envVars);
120
- await bootstrapImmutableProofMethod(engineCore, context, envVars);
123
+ await addVerificationMethod(engineCore, context, "attestation", envVars.attestationVerificationMethodId);
124
+ await addVerificationMethod(engineCore, context, "immutable proof", envVars.immutableProofVerificationMethodId);
125
+ await bootstrapSynchronisedStorage(engineCore, context, envVars);
121
126
  }
122
127
  /**
123
128
  * Bootstrap the node creating any necessary resources.
@@ -132,23 +137,21 @@ async function bootstrapNodeIdentity(engineCore, context, envVars, features) {
132
137
  // But we have a chicken and egg problem in that we can't create the identity
133
138
  // to store the mnemonic in the vault without an identity. We use a temporary identity
134
139
  // and then replace it with the new identity later in the process.
135
- const engineDefaultTypes = engineCore.getDefaultTypes();
136
- if (!Is.empty(engineDefaultTypes.vaultConnector)) {
137
- const vaultConnector = VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
138
- const workingIdentity = envVars.identity ??
139
- context.state.nodeIdentity ??
140
- `bootstrap-temp-${Converter.bytesToHex(RandomHelper.generate(16))}`;
141
- await bootstrapMnemonic(engineCore, envVars, features, vaultConnector, workingIdentity);
142
- const addresses = await bootstrapWallet(engineCore, envVars, features, workingIdentity);
143
- const finalIdentity = await bootstrapIdentity(engineCore, envVars, features, workingIdentity);
144
- await finaliseWallet(engineCore, envVars, features, finalIdentity, addresses);
145
- await finaliseMnemonic(vaultConnector, workingIdentity, finalIdentity);
146
- context.state.nodeIdentity = finalIdentity;
147
- context.stateDirty = true;
148
- engineCore.logInfo(I18n.formatMessage("node.nodeIdentity", {
149
- identity: context.state.nodeIdentity
150
- }));
151
- }
140
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
141
+ const vaultConnector = VaultConnectorFactory.get(defaultVaultConnectorType);
142
+ const workingIdentity = envVars.identity ??
143
+ context.state.nodeIdentity ??
144
+ `bootstrap-temp-${Converter.bytesToHex(RandomHelper.generate(16))}`;
145
+ await bootstrapMnemonic(engineCore, envVars, features, vaultConnector, workingIdentity);
146
+ const addresses = await bootstrapWallet(engineCore, envVars, features, workingIdentity);
147
+ const finalIdentity = await bootstrapIdentity(engineCore, envVars, features, workingIdentity);
148
+ await finaliseWallet(engineCore, envVars, features, finalIdentity, addresses);
149
+ await finaliseMnemonic(vaultConnector, workingIdentity, finalIdentity);
150
+ context.state.nodeIdentity = finalIdentity;
151
+ context.stateDirty = true;
152
+ engineCore.logInfo(I18n.formatMessage("node.nodeIdentity", {
153
+ identity: context.state.nodeIdentity
154
+ }));
152
155
  }
153
156
  }
154
157
  /**
@@ -160,12 +163,13 @@ async function bootstrapNodeIdentity(engineCore, context, envVars, features) {
160
163
  * @returns The addresses for the wallet.
161
164
  */
162
165
  async function bootstrapIdentity(engineCore, envVars, features, nodeIdentity) {
163
- const engineDefaultTypes = engineCore.getDefaultTypes();
166
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
164
167
  // Now create an identity for the node controlled by the address we just funded
165
- const identityConnector = IdentityConnectorFactory.get(engineDefaultTypes.identityConnector);
168
+ const identityConnector = IdentityConnectorFactory.get(defaultIdentityConnectorType);
166
169
  let identityDocument;
167
170
  try {
168
- const identityResolverConnector = IdentityResolverConnectorFactory.get(engineDefaultTypes.identityResolverConnector);
171
+ const defaultIdentityResolverConnectorType = engineCore.getRegisteredInstanceType("identityResolverConnector");
172
+ const identityResolverConnector = IdentityResolverConnectorFactory.get(defaultIdentityResolverConnectorType);
169
173
  identityDocument = await identityResolverConnector.resolveDocument(nodeIdentity);
170
174
  engineCore.logInfo(I18n.formatMessage("node.existingNodeIdentity", { identity: nodeIdentity }));
171
175
  }
@@ -175,7 +179,7 @@ async function bootstrapIdentity(engineCore, envVars, features, nodeIdentity) {
175
179
  identityDocument = await identityConnector.createDocument(nodeIdentity);
176
180
  engineCore.logInfo(I18n.formatMessage("node.createdNodeIdentity", { identity: identityDocument.id }));
177
181
  }
178
- if (engineDefaultTypes.identityConnector === IdentityConnectorType.Iota) {
182
+ if (defaultIdentityConnectorType.startsWith(IdentityConnectorType.Iota)) {
179
183
  const didUrn = Urn.fromValidString(identityDocument.id);
180
184
  const didParts = didUrn.parts();
181
185
  const objectId = didParts[3];
@@ -194,23 +198,26 @@ async function bootstrapIdentity(engineCore, envVars, features, nodeIdentity) {
194
198
  * @returns The addresses for the wallet.
195
199
  */
196
200
  async function bootstrapWallet(engineCore, envVars, features, nodeIdentity) {
197
- const engineDefaultTypes = engineCore.getDefaultTypes();
198
- const walletConnector = WalletConnectorFactory.get(engineDefaultTypes.walletConnector);
199
- const addresses = await walletConnector.getAddresses(nodeIdentity, 0, 0, 5);
200
- const balance = await walletConnector.getBalance(nodeIdentity, addresses[0]);
201
- if (balance === 0n) {
202
- let address0 = addresses[0];
203
- if (engineDefaultTypes.walletConnector === WalletConnectorType.Iota) {
204
- address0 = `${envVars.iotaExplorerEndpoint}address/${address0}?network=${envVars.iotaNetwork}`;
201
+ if (features.includes(NodeFeatures.NodeWallet)) {
202
+ const defaultWalletConnectorType = engineCore.getRegisteredInstanceType("walletConnector");
203
+ const walletConnector = WalletConnectorFactory.get(defaultWalletConnectorType);
204
+ const addresses = await walletConnector.getAddresses(nodeIdentity, 0, 0, 5);
205
+ const balance = await walletConnector.getBalance(nodeIdentity, addresses[0]);
206
+ if (balance === 0n) {
207
+ let address0 = addresses[0];
208
+ if (defaultWalletConnectorType.startsWith(WalletConnectorType.Iota)) {
209
+ address0 = `${envVars.iotaExplorerEndpoint}address/${address0}?network=${envVars.iotaNetwork}`;
210
+ }
211
+ engineCore.logInfo(I18n.formatMessage("node.fundingWallet", { address: address0 }));
212
+ // Add some funds to the wallet from the faucet
213
+ await walletConnector.ensureBalance(nodeIdentity, addresses[0], 1000000000n);
205
214
  }
206
- engineCore.logInfo(I18n.formatMessage("node.fundingWallet", { address: address0 }));
207
- // Add some funds to the wallet from the faucet
208
- await walletConnector.ensureBalance(nodeIdentity, addresses[0], 1000000000n);
209
- }
210
- else {
211
- engineCore.logInfo(I18n.formatMessage("node.fundedWallet"));
215
+ else {
216
+ engineCore.logInfo(I18n.formatMessage("node.fundedWallet"));
217
+ }
218
+ return addresses;
212
219
  }
213
- return addresses;
220
+ return [];
214
221
  }
215
222
  /**
216
223
  * Bootstrap the identity for the node.
@@ -221,15 +228,17 @@ async function bootstrapWallet(engineCore, envVars, features, nodeIdentity) {
221
228
  * @param addresses The addresses for the wallet.
222
229
  */
223
230
  async function finaliseWallet(engineCore, envVars, features, finalIdentity, addresses) {
224
- const engineDefaultTypes = engineCore.getDefaultTypes();
225
- // If we are using entity storage for wallet the identity associated with the
226
- // address will be wrong, so fix it
227
- if (engineDefaultTypes.walletConnector === WalletConnectorType.EntityStorage) {
228
- const walletAddress = EntityStorageConnectorFactory.get(StringHelper.kebabCase("WalletAddress"));
229
- const addr = await walletAddress.get(addresses[0]);
230
- if (!Is.empty(addr)) {
231
- addr.identity = finalIdentity;
232
- await walletAddress.set(addr);
231
+ if (features.includes(NodeFeatures.NodeWallet)) {
232
+ const defaultWalletConnectorType = engineCore.getRegisteredInstanceType("walletConnector");
233
+ // If we are using entity storage for wallet the identity associated with the
234
+ // address will be wrong, so fix it
235
+ if (defaultWalletConnectorType.startsWith(WalletConnectorType.EntityStorage)) {
236
+ const walletAddress = EntityStorageConnectorFactory.get(StringHelper.kebabCase("WalletAddress"));
237
+ const addr = await walletAddress.get(addresses[0]);
238
+ if (!Is.empty(addr)) {
239
+ addr.identity = finalIdentity;
240
+ await walletAddress.set(addr);
241
+ }
233
242
  }
234
243
  }
235
244
  }
@@ -291,8 +300,8 @@ async function finaliseMnemonic(vaultConnector, workingIdentity, finalIdentity)
291
300
  */
292
301
  async function bootstrapNodeUser(engineCore, context, envVars, features) {
293
302
  if (features.includes(NodeFeatures.NodeUser)) {
294
- const engineDefaultTypes = engineCore.getDefaultTypes();
295
- if (engineDefaultTypes.authenticationComponent === "entity-storage-authentication-service" &&
303
+ const defaultAuthenticationComponentType = engineCore.getRegisteredInstanceType("authenticationComponent");
304
+ if (defaultAuthenticationComponentType.startsWith(AuthenticationComponentType.EntityStorage) &&
296
305
  Is.stringValue(context.state.nodeIdentity)) {
297
306
  const authUserEntityStorage = EntityStorageConnectorFactory.get(StringHelper.kebabCase("AuthenticationUser"));
298
307
  const email = envVars.username ?? DEFAULT_NODE_USERNAME;
@@ -335,7 +344,8 @@ async function bootstrapNodeUser(engineCore, context, envVars, features) {
335
344
  }
336
345
  }
337
346
  // We have create a node user, now we need to create a profile for the user
338
- const identityProfileConnector = IdentityProfileConnectorFactory.get(engineDefaultTypes.identityProfileConnector);
347
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
348
+ const identityProfileConnector = IdentityProfileConnectorFactory.get(defaultIdentityConnectorType);
339
349
  if (identityProfileConnector) {
340
350
  let userProfile;
341
351
  try {
@@ -365,43 +375,6 @@ async function bootstrapNodeUser(engineCore, context, envVars, features) {
365
375
  }
366
376
  }
367
377
  }
368
- /**
369
- * Bootstrap the attestation verification methods.
370
- * @param engineCore The engine core for the node.
371
- * @param context The context for the node.
372
- * @param envVars The environment variables for the node.
373
- * @param features The features that are enabled on the node.
374
- */
375
- async function bootstrapAttestationMethod(engineCore, context, envVars, features) {
376
- if (Is.stringValue(context.state.nodeIdentity) &&
377
- Is.arrayValue(context.config.types.identityConnector) &&
378
- Is.stringValue(envVars.attestationVerificationMethodId)) {
379
- const engineDefaultTypes = engineCore.getDefaultTypes();
380
- const identityConnector = IdentityConnectorFactory.get(engineDefaultTypes.identityConnector);
381
- const identityResolverConnector = IdentityResolverConnectorFactory.get(engineDefaultTypes.identityResolverConnector);
382
- const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
383
- const fullMethodId = `${identityDocument.id}#${envVars.attestationVerificationMethodId}`;
384
- let createVm = true;
385
- try {
386
- DocumentHelper.getVerificationMethod(identityDocument, fullMethodId, "assertionMethod");
387
- createVm = false;
388
- }
389
- catch { }
390
- if (createVm) {
391
- // Add attestation verification method to DID, the correct node context is now in place
392
- // so the keys for the verification method will be stored correctly
393
- engineCore.logInfo(I18n.formatMessage("node.addingAttestation", {
394
- methodId: fullMethodId
395
- }));
396
- await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", envVars.attestationVerificationMethodId);
397
- }
398
- else {
399
- engineCore.logInfo(I18n.formatMessage("node.existingAttestation", {
400
- methodId: fullMethodId
401
- }));
402
- }
403
- }
404
- }
405
378
  /**
406
379
  * Bootstrap the immutable proof verification methods.
407
380
  * @param engineCore The engine core for the node.
@@ -409,36 +382,7 @@ async function bootstrapAttestationMethod(engineCore, context, envVars, features
409
382
  * @param envVars The environment variables for the node.
410
383
  * @param features The features that are enabled on the node.
411
384
  */
412
- async function bootstrapImmutableProofMethod(engineCore, context, envVars, features) {
413
- const engineDefaultTypes = engineCore.getDefaultTypes();
414
- if (Is.stringValue(context.state.nodeIdentity) &&
415
- Is.arrayValue(context.config.types.identityConnector) &&
416
- Is.stringValue(envVars.immutableProofVerificationMethodId)) {
417
- const identityConnector = IdentityConnectorFactory.get(engineDefaultTypes.identityConnector);
418
- const identityResolverConnector = IdentityResolverConnectorFactory.get(engineDefaultTypes.identityResolverConnector);
419
- const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
420
- const fullMethodId = `${identityDocument.id}#${envVars.immutableProofVerificationMethodId}`;
421
- let createVm = true;
422
- try {
423
- DocumentHelper.getVerificationMethod(identityDocument, fullMethodId, "assertionMethod");
424
- createVm = false;
425
- }
426
- catch { }
427
- if (createVm) {
428
- // Add AIG verification method to DID, the correct node context is now in place
429
- // so the keys for the verification method will be stored correctly
430
- engineCore.logInfo(I18n.formatMessage("node.addingImmutableProof", {
431
- methodId: fullMethodId
432
- }));
433
- await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", envVars.immutableProofVerificationMethodId);
434
- }
435
- else {
436
- engineCore.logInfo(I18n.formatMessage("node.existingImmutableProof", {
437
- methodId: fullMethodId
438
- }));
439
- }
440
- }
441
- }
385
+ async function bootstrapImmutableProofMethod(engineCore, context, envVars, features) { }
442
386
  /**
443
387
  * Bootstrap the keys for blob encryption.
444
388
  * @param engineCore The engine core for the node.
@@ -449,18 +393,28 @@ async function bootstrapImmutableProofMethod(engineCore, context, envVars, featu
449
393
  async function bootstrapBlobEncryption(engineCore, context, envVars, features) {
450
394
  if ((Coerce.boolean(envVars.blobStorageEnableEncryption) ?? false) &&
451
395
  Is.stringValue(context.state.nodeIdentity)) {
452
- const engineDefaultTypes = engineCore.getDefaultTypes();
453
396
  // Create a new key for encrypting blobs
454
- const vaultConnector = VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
455
- const keyName = `${context.state.nodeIdentity}/${envVars.blobStorageEncryptionKey}`;
397
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
398
+ const vaultConnector = VaultConnectorFactory.get(defaultVaultConnectorType);
399
+ const keyName = `${context.state.nodeIdentity}/${envVars.blobStorageEncryptionKeyId}`;
456
400
  let existingKey;
457
401
  try {
458
402
  existingKey = await vaultConnector.getKey(keyName);
459
403
  }
460
404
  catch { }
461
405
  if (Is.empty(existingKey)) {
462
- engineCore.logInfo(I18n.formatMessage("node.creatingBlobEncryptionKey", { keyName }));
463
- await vaultConnector.createKey(keyName, VaultKeyType.ChaCha20Poly1305);
406
+ if (Is.stringBase64(envVars.blobStorageSymmetricEncryptionKey)) {
407
+ engineCore.logInfo(I18n.formatMessage("node.addingBlobEncryptionKey", { keyName }));
408
+ await vaultConnector.addKey(keyName, VaultKeyType.ChaCha20Poly1305, Converter.base64ToBytes(envVars.blobStorageSymmetricEncryptionKey));
409
+ }
410
+ else {
411
+ engineCore.logInfo(I18n.formatMessage("node.creatingBlobEncryptionKey", { keyName }));
412
+ const key = await vaultConnector.createKey(keyName, VaultKeyType.ChaCha20Poly1305);
413
+ engineCore.logInfo(I18n.formatMessage("node.createdBlobEncryptionKey", {
414
+ keyName,
415
+ keyValue: Converter.bytesToBase64(key)
416
+ }));
417
+ }
464
418
  }
465
419
  else {
466
420
  engineCore.logInfo(I18n.formatMessage("node.existingBlobEncryptionKey", { keyName }));
@@ -475,11 +429,13 @@ async function bootstrapBlobEncryption(engineCore, context, envVars, features) {
475
429
  * @param features The features that are enabled on the node.
476
430
  */
477
431
  async function bootstrapAuth(engineCore, context, envVars, features) {
478
- const engineDefaultTypes = engineCore.getDefaultTypes();
479
- if (engineDefaultTypes.authenticationComponent === "entity-storage-authentication-service" &&
432
+ const defaultAuthenticationComponentType = engineCore.getRegisteredInstanceTypeOptional("authenticationComponent");
433
+ if (Is.stringValue(defaultAuthenticationComponentType) &&
434
+ defaultAuthenticationComponentType.startsWith(AuthenticationComponentType.EntityStorage) &&
480
435
  Is.stringValue(context.state.nodeIdentity)) {
481
436
  // Create a new JWT signing key and a user login for the node
482
- const vaultConnector = VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
437
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
438
+ const vaultConnector = VaultConnectorFactory.get(defaultVaultConnectorType);
483
439
  const keyName = `${context.state.nodeIdentity}/${envVars.authSigningKeyId}`;
484
440
  let existingKey;
485
441
  try {
@@ -495,6 +451,76 @@ async function bootstrapAuth(engineCore, context, envVars, features) {
495
451
  }
496
452
  }
497
453
  }
454
+ /**
455
+ * Bootstrap the synchronised storage blob encryption and verification methods.
456
+ * @param engineCore The engine core for the node.
457
+ * @param context The context for the node.
458
+ * @param envVars The environment variables for the node.
459
+ * @param features The features that are enabled on the node.
460
+ */
461
+ async function bootstrapSynchronisedStorage(engineCore, context, envVars, features) {
462
+ if (Coerce.boolean(envVars.synchronisedStorageEnabled) ?? false) {
463
+ // Add the verification method to the identity if it doesn't exist
464
+ await addVerificationMethod(engineCore, context, "synchronised storage", envVars.synchronisedStorageVerificationMethodId);
465
+ // If this is a trusted node we need to add the blob encryption key pair
466
+ if (Is.stringValue(envVars.synchronisedStorageBlobStorageEncryptionKeyId) &&
467
+ Is.stringBase64(envVars.synchronisedStorageBlobStorageKey)) {
468
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
469
+ const vaultConnector = VaultConnectorFactory.get(defaultVaultConnectorType);
470
+ const keyName = envVars.synchronisedStorageBlobStorageEncryptionKeyId;
471
+ let existingKey;
472
+ try {
473
+ existingKey = await vaultConnector.getKey(keyName);
474
+ }
475
+ catch { }
476
+ if (Is.empty(existingKey)) {
477
+ engineCore.logInfo(I18n.formatMessage("node.addingSynchronisedStorageBlobEncryptionKey", { keyName }));
478
+ await vaultConnector.addKey(keyName, VaultKeyType.ChaCha20Poly1305, Converter.base64ToBytes(envVars.synchronisedStorageBlobStorageKey));
479
+ }
480
+ else {
481
+ engineCore.logInfo(I18n.formatMessage("node.existingSynchronisedStorageBlobEncryptionKey", { keyName }));
482
+ }
483
+ }
484
+ }
485
+ }
486
+ /**
487
+ * Add a verification method if it doesn't exist.
488
+ * @param engineCore The engine core for the node.
489
+ * @param context The context for the node.
490
+ * @param verificationMethodTitle The verification method title.
491
+ * @param verificationMethodId The verification method ID.
492
+ */
493
+ async function addVerificationMethod(engineCore, context, verificationMethodTitle, verificationMethodId) {
494
+ if (Is.stringValue(context.state.nodeIdentity) &&
495
+ Is.arrayValue(context.config.types.identityConnector) &&
496
+ Is.stringValue(verificationMethodId)) {
497
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
498
+ const identityConnector = IdentityConnectorFactory.get(defaultIdentityConnectorType);
499
+ const defaultIdentityResolverConnectorType = engineCore.getRegisteredInstanceType("identityResolverConnector");
500
+ const identityResolverConnector = IdentityResolverConnectorFactory.get(defaultIdentityResolverConnectorType);
501
+ const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
502
+ const fullMethodId = `${identityDocument.id}#${verificationMethodId}`;
503
+ let exists = false;
504
+ try {
505
+ DocumentHelper.getVerificationMethod(identityDocument, fullMethodId, "assertionMethod");
506
+ exists = true;
507
+ }
508
+ catch { }
509
+ if (!exists) {
510
+ engineCore.logInfo(I18n.formatMessage("node.addingVerificationMethod", {
511
+ title: verificationMethodTitle,
512
+ methodId: fullMethodId
513
+ }));
514
+ await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", verificationMethodId);
515
+ }
516
+ else {
517
+ engineCore.logInfo(I18n.formatMessage("node.existingVerificationMethod", {
518
+ title: verificationMethodTitle,
519
+ methodId: fullMethodId
520
+ }));
521
+ }
522
+ }
523
+ }
498
524
 
499
525
  // Copyright 2024 IOTA Stiftung.
500
526
  // SPDX-License-Identifier: Apache-2.0.
@@ -512,7 +538,9 @@ function buildEngineConfiguration(envVars) {
512
538
  envVars.attestationVerificationMethodId ??= "attestation-assertion";
513
539
  envVars.immutableProofVerificationMethodId ??= "immutable-proof-assertion";
514
540
  envVars.blobStorageEnableEncryption ??= "false";
515
- envVars.blobStorageEncryptionKey ??= "blob-encryption";
541
+ envVars.blobStorageEncryptionKeyId ??= "blob-encryption";
542
+ envVars.synchronisedStorageBlobStorageEncryptionKeyId ??= "synchronised-storage-blob-encryption";
543
+ envVars.synchronisedStorageVerificationMethodId ??= "synchronised-storage-assertion";
516
544
  const coreConfig = {
517
545
  debug: Coerce.boolean(envVars.debug) ?? false,
518
546
  types: {}
@@ -538,9 +566,10 @@ function buildEngineConfiguration(envVars) {
538
566
  configureAuditableItemGraph(coreConfig);
539
567
  configureAuditableItemStream(coreConfig);
540
568
  configureDocumentManagement(coreConfig);
541
- configureFederatedCatalogue(coreConfig, envVars);
542
569
  configureRightsManagement(coreConfig, envVars);
543
570
  configureTaskScheduler(coreConfig, envVars);
571
+ configureSynchronisedStorage(coreConfig, envVars);
572
+ configureFederatedCatalogue(coreConfig, envVars);
544
573
  return coreConfig;
545
574
  }
546
575
  /**
@@ -662,7 +691,7 @@ function configureEntityStorage(coreConfig, envVars) {
662
691
  }
663
692
  });
664
693
  }
665
- if (entityStorageConnectorTypes) {
694
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.PostgreSql)) {
666
695
  coreConfig.types.entityStorageConnector.push({
667
696
  type: EntityStorageConnectorType.PostgreSql,
668
697
  options: {
@@ -677,10 +706,19 @@ function configureEntityStorage(coreConfig, envVars) {
677
706
  }
678
707
  });
679
708
  }
709
+ const defaultEntityStorageConnectorType = envVars.entityStorageConnectorDefault ?? entityStorageConnectorTypes[0];
710
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.Synchronised)) {
711
+ // For synchronised storage we use the default connector as the one we wrap for real DB operations
712
+ coreConfig.types.entityStorageConnector.push({
713
+ type: EntityStorageConnectorType.Synchronised,
714
+ options: {
715
+ entityStorageConnectorType: defaultEntityStorageConnectorType
716
+ }
717
+ });
718
+ }
680
719
  if (Is.arrayValue(entityStorageConnectorTypes)) {
681
- const defaultStorageConnectorType = envVars.entityStorageConnectorDefault ?? entityStorageConnectorTypes[0];
682
720
  for (const config of coreConfig.types.entityStorageConnector) {
683
- if (config.type === defaultStorageConnectorType) {
721
+ if (config.type === defaultEntityStorageConnectorType) {
684
722
  config.isDefault = true;
685
723
  break;
686
724
  }
@@ -772,6 +810,13 @@ function configureBlobStorage(coreConfig, envVars) {
772
810
  for (const config of coreConfig.types.blobStorageConnector) {
773
811
  if (config.type === defaultStorageConnectorType) {
774
812
  config.isDefault = true;
813
+ }
814
+ // If this blob storage connector is the one to use for public access
815
+ // then add it as a feature
816
+ if (Is.stringValue(envVars.blobStorageConnectorPublic) &&
817
+ config.type === envVars.blobStorageConnectorPublic) {
818
+ config.features ??= [];
819
+ config.features.push("public");
775
820
  break;
776
821
  }
777
822
  }
@@ -783,7 +828,7 @@ function configureBlobStorage(coreConfig, envVars) {
783
828
  options: {
784
829
  config: {
785
830
  vaultKeyId: (envVars.blobStorageEnableEncryption ?? false)
786
- ? envVars.blobStorageEncryptionKey
831
+ ? envVars.blobStorageEncryptionKeyId
787
832
  : undefined
788
833
  }
789
834
  }
@@ -1277,25 +1322,6 @@ function configureDocumentManagement(coreConfig, envVars) {
1277
1322
  });
1278
1323
  }
1279
1324
  }
1280
- /**
1281
- * Configures the federated catalogue.
1282
- * @param coreConfig The core config.
1283
- * @param envVars The environment variables.
1284
- */
1285
- function configureFederatedCatalogue(coreConfig, envVars) {
1286
- if (Is.arrayValue(coreConfig.types.identityResolverComponent)) {
1287
- coreConfig.types.federatedCatalogueComponent ??= [];
1288
- coreConfig.types.federatedCatalogueComponent.push({
1289
- type: FederatedCatalogueComponentType.Service,
1290
- options: {
1291
- config: {
1292
- subResourceCacheTtlMs: Coerce.number(envVars.federatedCatalogueCacheTtlMs),
1293
- clearingHouseApproverList: Coerce.object(envVars.federatedCatalogueClearingHouseApproverList) ?? []
1294
- }
1295
- }
1296
- });
1297
- }
1298
- }
1299
1325
  /**
1300
1326
  * Configures the rights management.
1301
1327
  * @param coreConfig The core config.
@@ -1319,13 +1345,76 @@ function configureRightsManagement(coreConfig, envVars) {
1319
1345
  * @param envVars The environment variables.
1320
1346
  */
1321
1347
  function configureTaskScheduler(coreConfig, envVars) {
1322
- if (Coerce.boolean(envVars.taskSchedulerEnabled) ?? true) {
1348
+ if (Coerce.boolean(envVars.taskSchedulerEnabled) ?? false) {
1323
1349
  coreConfig.types.taskSchedulerComponent ??= [];
1324
1350
  coreConfig.types.taskSchedulerComponent.push({
1325
1351
  type: TaskSchedulerComponentType.Service
1326
1352
  });
1327
1353
  }
1328
1354
  }
1355
+ /**
1356
+ * Configures the synchronised storage.
1357
+ * @param coreConfig The core config.
1358
+ * @param envVars The environment variables.
1359
+ */
1360
+ function configureSynchronisedStorage(coreConfig, envVars) {
1361
+ if (Is.arrayValue(coreConfig.types.identityResolverComponent) &&
1362
+ (Coerce.boolean(envVars.synchronisedStorageEnabled) ?? false)) {
1363
+ // Check if the config provides a custom verifiable storage key id
1364
+ let verifiableStorageKeyId = Coerce.string(envVars.synchronisedStorageVerifiableStorageKeyId);
1365
+ if (!Is.stringValue(verifiableStorageKeyId)) {
1366
+ // No custom key so default to the network setting
1367
+ verifiableStorageKeyId = envVars.iotaNetwork;
1368
+ }
1369
+ coreConfig.types.synchronisedStorageComponent ??= [];
1370
+ coreConfig.types.synchronisedStorageComponent.push({
1371
+ type: SynchronisedStorageComponentType.Service,
1372
+ options: {
1373
+ config: {
1374
+ verifiableStorageKeyId: verifiableStorageKeyId ?? "",
1375
+ synchronisedStorageMethodId: envVars.synchronisedStorageVerificationMethodId,
1376
+ blobStorageEncryptionKeyId: envVars.synchronisedStorageBlobStorageEncryptionKeyId,
1377
+ entityUpdateIntervalMinutes: Coerce.number(envVars.synchronisedStorageEntityUpdateIntervalMinutes),
1378
+ consolidationIntervalMinutes: Coerce.number(envVars.synchronisedStorageConsolidationIntervalMinutes),
1379
+ consolidationBatchSize: Coerce.number(envVars.synchronisedStorageConsolidationBatchSize),
1380
+ maxConsolidations: Coerce.number(envVars.synchronisedStorageMaxConsolidations)
1381
+ }
1382
+ }
1383
+ });
1384
+ // If there is a trusted url set, we need to add a client
1385
+ // and give it a feature of trusted so that when the synchronised
1386
+ // storage is created it can pickup the correct component
1387
+ if (Is.stringValue(envVars.synchronisedStorageTrustedUrl)) {
1388
+ coreConfig.types.synchronisedStorageComponent.push({
1389
+ type: SynchronisedStorageComponentType.RestClient,
1390
+ options: {
1391
+ endpoint: envVars.synchronisedStorageTrustedUrl
1392
+ },
1393
+ features: ["trusted"]
1394
+ });
1395
+ }
1396
+ }
1397
+ }
1398
+ /**
1399
+ * Configures the federated catalogue.
1400
+ * @param coreConfig The core config.
1401
+ * @param envVars The environment variables.
1402
+ */
1403
+ function configureFederatedCatalogue(coreConfig, envVars) {
1404
+ if (Is.arrayValue(coreConfig.types.identityResolverComponent) &&
1405
+ (Coerce.boolean(envVars.federatedCatalogueEnabled) ?? false)) {
1406
+ coreConfig.types.federatedCatalogueComponent ??= [];
1407
+ coreConfig.types.federatedCatalogueComponent.push({
1408
+ type: FederatedCatalogueComponentType.Service,
1409
+ options: {
1410
+ config: {
1411
+ subResourceCacheTtlMs: Coerce.number(envVars.federatedCatalogueCacheTtlMs),
1412
+ clearingHouseApproverList: Coerce.object(envVars.federatedCatalogueClearingHouseApproverList) ?? []
1413
+ }
1414
+ }
1415
+ });
1416
+ }
1417
+ }
1329
1418
  /**
1330
1419
  * Configures the DLT.
1331
1420
  * @param coreConfig The core config.
@@ -1365,9 +1454,10 @@ function configureDlt(coreConfig, envVars) {
1365
1454
  * @param coreEngineConfig The core engine config.
1366
1455
  * @param serverInfo The server information.
1367
1456
  * @param openApiSpecPath The path to the open api spec.
1457
+ * @param favIconPath The path to the favicon.
1368
1458
  * @returns The the config for the core and the server.
1369
1459
  */
1370
- function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, openApiSpecPath) {
1460
+ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, openApiSpecPath, favIconPath) {
1371
1461
  envVars.authSigningKeyId ??= "auth-signing";
1372
1462
  const webServerOptions = {
1373
1463
  port: Coerce.number(envVars.port),
@@ -1394,7 +1484,8 @@ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, o
1394
1484
  options: {
1395
1485
  config: {
1396
1486
  serverInfo,
1397
- openApiSpecPath
1487
+ openApiSpecPath,
1488
+ favIconPath
1398
1489
  }
1399
1490
  }
1400
1491
  }
@@ -1511,9 +1602,10 @@ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, o
1511
1602
  * @returns The engine server.
1512
1603
  */
1513
1604
  async function start(nodeOptions, engineServerConfig, envVars) {
1514
- envVars.storageFileRoot ??= "";
1515
1605
  const entityStorageConnectorType = envVars.entityStorageConnectorType?.split(",") ?? [];
1516
1606
  const blobStorageConnectorType = envVars.blobStorageConnectorType?.split(",") ?? [];
1607
+ // If the blob storage or entity storage is configured with file connectors
1608
+ // then we need to make sure the storageFileRoot is set
1517
1609
  if ((entityStorageConnectorType.includes(EntityStorageConnectorType.File) ||
1518
1610
  blobStorageConnectorType.includes(BlobStorageConnectorType.File) ||
1519
1611
  Is.empty(nodeOptions?.stateStorage)) &&
@@ -1522,7 +1614,7 @@ async function start(nodeOptions, engineServerConfig, envVars) {
1522
1614
  storageFileRoot: `${nodeOptions?.envPrefix ?? ""}_STORAGE_FILE_ROOT`
1523
1615
  });
1524
1616
  }
1525
- // Create the engine instance using file state storage and custom bootstrap.
1617
+ // Create the engine instance using file state storage unless one is configured in options
1526
1618
  const engine = new Engine({
1527
1619
  config: engineServerConfig,
1528
1620
  stateStorage: nodeOptions?.stateStorage ?? new FileStateStorage(envVars.stateFilename ?? ""),
@@ -1566,7 +1658,7 @@ async function run(nodeOptions) {
1566
1658
  nodeOptions ??= {};
1567
1659
  const serverInfo = {
1568
1660
  name: nodeOptions?.serverName ?? "TWIN Node Server",
1569
- version: nodeOptions?.serverVersion ?? "0.0.2-next.3" // x-release-please-version
1661
+ version: nodeOptions?.serverVersion ?? "0.0.2-next.5" // x-release-please-version
1570
1662
  };
1571
1663
  console.log(`\u001B[4mđŸŒŠī¸ ${serverInfo.name} v${serverInfo.version}\u001B[24m\n`);
1572
1664
  if (!Is.stringValue(nodeOptions?.executionDirectory)) {
@@ -1579,13 +1671,21 @@ async function run(nodeOptions) {
1579
1671
  console.info("Locales Directory:", nodeOptions.localesDirectory);
1580
1672
  await initialiseLocales(nodeOptions.localesDirectory);
1581
1673
  if (Is.empty(nodeOptions?.openApiSpecFile)) {
1582
- const specFile = path.resolve(path.join(nodeOptions.executionDirectory, "docs", "open-api", "spec.json"));
1674
+ const specFile = path.resolve(path.join(nodeOptions.executionDirectory ?? "", "docs", "open-api", "spec.json"));
1583
1675
  console.info("Default OpenAPI Spec File:", specFile);
1584
1676
  if (await fileExists(specFile)) {
1585
1677
  nodeOptions ??= {};
1586
1678
  nodeOptions.openApiSpecFile = specFile;
1587
1679
  }
1588
1680
  }
1681
+ if (Is.empty(nodeOptions?.favIconFile)) {
1682
+ const favIconFile = path.resolve(path.join(nodeOptions.executionDirectory ?? "", "static", "favicon.png"));
1683
+ console.info("Default Favicon File:", favIconFile);
1684
+ if (await fileExists(favIconFile)) {
1685
+ nodeOptions ??= {};
1686
+ nodeOptions.favIconFile = favIconFile;
1687
+ }
1688
+ }
1589
1689
  nodeOptions.envPrefix ??= "TWIN_NODE_";
1590
1690
  console.info("Environment Prefix:", nodeOptions.envPrefix);
1591
1691
  const { engineServerConfig, nodeEnvVars: envVars } = await buildConfiguration(process.env, nodeOptions, serverInfo);
@@ -1624,7 +1724,8 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1624
1724
  }
1625
1725
  if (Is.arrayValue(options?.envFilenames)) {
1626
1726
  const output = dotenv.config({
1627
- path: options?.envFilenames
1727
+ path: options?.envFilenames,
1728
+ quiet: true
1628
1729
  });
1629
1730
  // We don't want to throw an error if the default environment file is not found.
1630
1731
  // Only if we have custom environment files.
@@ -1654,7 +1755,7 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1654
1755
  }
1655
1756
  // Build the engine configuration from the environment variables.
1656
1757
  const coreConfig = buildEngineConfiguration(envVars);
1657
- const engineServerConfig = buildEngineServerConfiguration(envVars, coreConfig, serverInfo, options?.openApiSpecFile);
1758
+ const engineServerConfig = buildEngineServerConfiguration(envVars, coreConfig, serverInfo, options?.openApiSpecFile, options?.favIconFile);
1658
1759
  // Merge any custom configuration provided in the options.
1659
1760
  if (Is.arrayValue(options?.configFilenames)) {
1660
1761
  for (const configFile of options.configFilenames) {
@@ -1676,4 +1777,4 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1676
1777
  return { engineServerConfig, nodeEnvVars: envVars };
1677
1778
  }
1678
1779
 
1679
- export { NodeFeatures, bootstrap, bootstrapAttestationMethod, bootstrapAuth, bootstrapBlobEncryption, bootstrapImmutableProofMethod, bootstrapNodeIdentity, bootstrapNodeUser, buildConfiguration, buildEngineConfiguration, buildEngineServerConfiguration, fileExists, getExecutionDirectory, getFeatures, initialiseLocales, loadJsonFile, run, start };
1780
+ export { NodeFeatures, bootstrap, bootstrapAuth, bootstrapBlobEncryption, bootstrapImmutableProofMethod, bootstrapNodeIdentity, bootstrapNodeUser, bootstrapSynchronisedStorage, buildConfiguration, buildEngineConfiguration, buildEngineServerConfiguration, fileExists, getExecutionDirectory, getFeatures, initialiseLocales, loadJsonFile, run, start };