@twin.org/node-core 0.0.2-next.2 → 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.
@@ -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 === "entity-storage") {
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 === "authentication-entity-storage" &&
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 === "authentication-entity-storage" &&
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,77 @@ 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.synchronisedStorageBlobStoragePrivateKey) &&
468
+ Is.stringBase64(envVars.synchronisedStorageBlobStoragePublicKey)) {
469
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
470
+ const vaultConnector = VaultConnectorFactory.get(defaultVaultConnectorType);
471
+ const keyName = envVars.synchronisedStorageBlobStorageEncryptionKeyId;
472
+ let existingKey;
473
+ try {
474
+ existingKey = await vaultConnector.getKey(keyName);
475
+ }
476
+ catch { }
477
+ if (Is.empty(existingKey)) {
478
+ engineCore.logInfo(I18n.formatMessage("node.addingSynchronisedStorageBlobEncryptionKey", { keyName }));
479
+ await vaultConnector.addKey(keyName, VaultKeyType.Rsa2048, Converter.base64ToBytes(envVars.synchronisedStorageBlobStoragePrivateKey), Converter.base64ToBytes(envVars.synchronisedStorageBlobStoragePublicKey));
480
+ }
481
+ else {
482
+ engineCore.logInfo(I18n.formatMessage("node.existingSynchronisedStorageBlobEncryptionKey", { keyName }));
483
+ }
484
+ }
485
+ }
486
+ }
487
+ /**
488
+ * Add a verification method if it doesn't exist.
489
+ * @param engineCore The engine core for the node.
490
+ * @param context The context for the node.
491
+ * @param verificationMethodTitle The verification method title.
492
+ * @param verificationMethodId The verification method ID.
493
+ */
494
+ async function addVerificationMethod(engineCore, context, verificationMethodTitle, verificationMethodId) {
495
+ if (Is.stringValue(context.state.nodeIdentity) &&
496
+ Is.arrayValue(context.config.types.identityConnector) &&
497
+ Is.stringValue(verificationMethodId)) {
498
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
499
+ const identityConnector = IdentityConnectorFactory.get(defaultIdentityConnectorType);
500
+ const defaultIdentityResolverConnectorType = engineCore.getRegisteredInstanceType("identityResolverConnector");
501
+ const identityResolverConnector = IdentityResolverConnectorFactory.get(defaultIdentityResolverConnectorType);
502
+ const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
503
+ const fullMethodId = `${identityDocument.id}#${verificationMethodId}`;
504
+ let exists = false;
505
+ try {
506
+ DocumentHelper.getVerificationMethod(identityDocument, fullMethodId, "assertionMethod");
507
+ exists = true;
508
+ }
509
+ catch { }
510
+ if (!exists) {
511
+ engineCore.logInfo(I18n.formatMessage("node.addingVerificationMethod", {
512
+ title: verificationMethodTitle,
513
+ methodId: fullMethodId
514
+ }));
515
+ await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", verificationMethodId);
516
+ }
517
+ else {
518
+ engineCore.logInfo(I18n.formatMessage("node.existingVerificationMethod", {
519
+ title: verificationMethodTitle,
520
+ methodId: fullMethodId
521
+ }));
522
+ }
523
+ }
524
+ }
498
525
 
499
526
  // Copyright 2024 IOTA Stiftung.
500
527
  // SPDX-License-Identifier: Apache-2.0.
@@ -512,7 +539,9 @@ function buildEngineConfiguration(envVars) {
512
539
  envVars.attestationVerificationMethodId ??= "attestation-assertion";
513
540
  envVars.immutableProofVerificationMethodId ??= "immutable-proof-assertion";
514
541
  envVars.blobStorageEnableEncryption ??= "false";
515
- envVars.blobStorageEncryptionKey ??= "blob-encryption";
542
+ envVars.blobStorageEncryptionKeyId ??= "blob-encryption";
543
+ envVars.synchronisedStorageBlobStorageEncryptionKeyId ??= "synchronised-storage-blob-encryption";
544
+ envVars.synchronisedStorageVerificationMethodId ??= "synchronised-storage-assertion";
516
545
  const coreConfig = {
517
546
  debug: Coerce.boolean(envVars.debug) ?? false,
518
547
  types: {}
@@ -538,9 +567,10 @@ function buildEngineConfiguration(envVars) {
538
567
  configureAuditableItemGraph(coreConfig);
539
568
  configureAuditableItemStream(coreConfig);
540
569
  configureDocumentManagement(coreConfig);
541
- configureFederatedCatalogue(coreConfig, envVars);
542
570
  configureRightsManagement(coreConfig, envVars);
543
571
  configureTaskScheduler(coreConfig, envVars);
572
+ configureSynchronisedStorage(coreConfig, envVars);
573
+ configureFederatedCatalogue(coreConfig, envVars);
544
574
  return coreConfig;
545
575
  }
546
576
  /**
@@ -662,7 +692,7 @@ function configureEntityStorage(coreConfig, envVars) {
662
692
  }
663
693
  });
664
694
  }
665
- if (entityStorageConnectorTypes) {
695
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.PostgreSql)) {
666
696
  coreConfig.types.entityStorageConnector.push({
667
697
  type: EntityStorageConnectorType.PostgreSql,
668
698
  options: {
@@ -677,10 +707,19 @@ function configureEntityStorage(coreConfig, envVars) {
677
707
  }
678
708
  });
679
709
  }
710
+ const defaultEntityStorageConnectorType = envVars.entityStorageConnectorDefault ?? entityStorageConnectorTypes[0];
711
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.Synchronised)) {
712
+ // For synchronised storage we use the default connector as the one we wrap for real DB operations
713
+ coreConfig.types.entityStorageConnector.push({
714
+ type: EntityStorageConnectorType.Synchronised,
715
+ options: {
716
+ entityStorageConnectorType: defaultEntityStorageConnectorType
717
+ }
718
+ });
719
+ }
680
720
  if (Is.arrayValue(entityStorageConnectorTypes)) {
681
- const defaultStorageConnectorType = envVars.entityStorageConnectorDefault ?? entityStorageConnectorTypes[0];
682
721
  for (const config of coreConfig.types.entityStorageConnector) {
683
- if (config.type === defaultStorageConnectorType) {
722
+ if (config.type === defaultEntityStorageConnectorType) {
684
723
  config.isDefault = true;
685
724
  break;
686
725
  }
@@ -772,6 +811,13 @@ function configureBlobStorage(coreConfig, envVars) {
772
811
  for (const config of coreConfig.types.blobStorageConnector) {
773
812
  if (config.type === defaultStorageConnectorType) {
774
813
  config.isDefault = true;
814
+ }
815
+ // If this blob storage connector is the one to use for public access
816
+ // then add it as a feature
817
+ if (Is.stringValue(envVars.blobStorageConnectorPublic) &&
818
+ config.type === envVars.blobStorageConnectorPublic) {
819
+ config.features ??= [];
820
+ config.features.push("public");
775
821
  break;
776
822
  }
777
823
  }
@@ -783,7 +829,7 @@ function configureBlobStorage(coreConfig, envVars) {
783
829
  options: {
784
830
  config: {
785
831
  vaultKeyId: (envVars.blobStorageEnableEncryption ?? false)
786
- ? envVars.blobStorageEncryptionKey
832
+ ? envVars.blobStorageEncryptionKeyId
787
833
  : undefined
788
834
  }
789
835
  }
@@ -1277,25 +1323,6 @@ function configureDocumentManagement(coreConfig, envVars) {
1277
1323
  });
1278
1324
  }
1279
1325
  }
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
1326
  /**
1300
1327
  * Configures the rights management.
1301
1328
  * @param coreConfig The core config.
@@ -1319,10 +1346,73 @@ function configureRightsManagement(coreConfig, envVars) {
1319
1346
  * @param envVars The environment variables.
1320
1347
  */
1321
1348
  function configureTaskScheduler(coreConfig, envVars) {
1322
- if (Coerce.boolean(envVars.taskSchedulerEnabled) ?? true) {
1349
+ if (Coerce.boolean(envVars.taskSchedulerEnabled) ?? false) {
1323
1350
  coreConfig.types.taskSchedulerComponent ??= [];
1324
1351
  coreConfig.types.taskSchedulerComponent.push({
1325
- type: TaskSchedulerComponentType.Default
1352
+ type: TaskSchedulerComponentType.Service
1353
+ });
1354
+ }
1355
+ }
1356
+ /**
1357
+ * Configures the synchronised storage.
1358
+ * @param coreConfig The core config.
1359
+ * @param envVars The environment variables.
1360
+ */
1361
+ function configureSynchronisedStorage(coreConfig, envVars) {
1362
+ if (Is.arrayValue(coreConfig.types.identityResolverComponent) &&
1363
+ (Coerce.boolean(envVars.synchronisedStorageEnabled) ?? false)) {
1364
+ // Check if the config provides a custom verifiable storage key id
1365
+ let verifiableStorageKeyId = Coerce.string(envVars.synchronisedStorageVerifiableStorageKeyId);
1366
+ if (!Is.stringValue(verifiableStorageKeyId)) {
1367
+ // No custom key so default to the network setting
1368
+ verifiableStorageKeyId = envVars.iotaNetwork;
1369
+ }
1370
+ coreConfig.types.synchronisedStorageComponent ??= [];
1371
+ coreConfig.types.synchronisedStorageComponent.push({
1372
+ type: SynchronisedStorageComponentType.Service,
1373
+ options: {
1374
+ config: {
1375
+ verifiableStorageKeyId: verifiableStorageKeyId ?? "",
1376
+ synchronisedStorageMethodId: envVars.synchronisedStorageVerificationMethodId,
1377
+ blobStorageEncryptionKeyId: envVars.synchronisedStorageBlobStorageEncryptionKeyId,
1378
+ entityUpdateIntervalMinutes: Coerce.number(envVars.synchronisedStorageEntityUpdateIntervalMinutes),
1379
+ consolidationIntervalMinutes: Coerce.number(envVars.synchronisedStorageConsolidationIntervalMinutes),
1380
+ consolidationBatchSize: Coerce.number(envVars.synchronisedStorageConsolidationBatchSize),
1381
+ maxConsolidations: Coerce.number(envVars.synchronisedStorageMaxConsolidations)
1382
+ }
1383
+ }
1384
+ });
1385
+ // If there is a trusted url set, we need to add a client
1386
+ // and give it a feature of trusted so that when the synchronised
1387
+ // storage is created it can pickup the correct component
1388
+ if (Is.stringValue(envVars.synchronisedStorageTrustedUrl)) {
1389
+ coreConfig.types.synchronisedStorageComponent.push({
1390
+ type: SynchronisedStorageComponentType.RestClient,
1391
+ options: {
1392
+ endpoint: envVars.synchronisedStorageTrustedUrl
1393
+ },
1394
+ features: ["trusted"]
1395
+ });
1396
+ }
1397
+ }
1398
+ }
1399
+ /**
1400
+ * Configures the federated catalogue.
1401
+ * @param coreConfig The core config.
1402
+ * @param envVars The environment variables.
1403
+ */
1404
+ function configureFederatedCatalogue(coreConfig, envVars) {
1405
+ if (Is.arrayValue(coreConfig.types.identityResolverComponent) &&
1406
+ (Coerce.boolean(envVars.federatedCatalogueEnabled) ?? false)) {
1407
+ coreConfig.types.federatedCatalogueComponent ??= [];
1408
+ coreConfig.types.federatedCatalogueComponent.push({
1409
+ type: FederatedCatalogueComponentType.Service,
1410
+ options: {
1411
+ config: {
1412
+ subResourceCacheTtlMs: Coerce.number(envVars.federatedCatalogueCacheTtlMs),
1413
+ clearingHouseApproverList: Coerce.object(envVars.federatedCatalogueClearingHouseApproverList) ?? []
1414
+ }
1415
+ }
1326
1416
  });
1327
1417
  }
1328
1418
  }
@@ -1511,9 +1601,10 @@ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, o
1511
1601
  * @returns The engine server.
1512
1602
  */
1513
1603
  async function start(nodeOptions, engineServerConfig, envVars) {
1514
- envVars.storageFileRoot ??= "";
1515
1604
  const entityStorageConnectorType = envVars.entityStorageConnectorType?.split(",") ?? [];
1516
1605
  const blobStorageConnectorType = envVars.blobStorageConnectorType?.split(",") ?? [];
1606
+ // If the blob storage or entity storage is configured with file connectors
1607
+ // then we need to make sure the storageFileRoot is set
1517
1608
  if ((entityStorageConnectorType.includes(EntityStorageConnectorType.File) ||
1518
1609
  blobStorageConnectorType.includes(BlobStorageConnectorType.File) ||
1519
1610
  Is.empty(nodeOptions?.stateStorage)) &&
@@ -1522,7 +1613,7 @@ async function start(nodeOptions, engineServerConfig, envVars) {
1522
1613
  storageFileRoot: `${nodeOptions?.envPrefix ?? ""}_STORAGE_FILE_ROOT`
1523
1614
  });
1524
1615
  }
1525
- // Create the engine instance using file state storage and custom bootstrap.
1616
+ // Create the engine instance using file state storage unless one is configured in options
1526
1617
  const engine = new Engine({
1527
1618
  config: engineServerConfig,
1528
1619
  stateStorage: nodeOptions?.stateStorage ?? new FileStateStorage(envVars.stateFilename ?? ""),
@@ -1566,7 +1657,7 @@ async function run(nodeOptions) {
1566
1657
  nodeOptions ??= {};
1567
1658
  const serverInfo = {
1568
1659
  name: nodeOptions?.serverName ?? "TWIN Node Server",
1569
- version: nodeOptions?.serverVersion ?? "0.0.2-next.2" // x-release-please-version
1660
+ version: nodeOptions?.serverVersion ?? "0.0.2-next.4" // x-release-please-version
1570
1661
  };
1571
1662
  console.log(`\u001B[4mđŸŒŠī¸ ${serverInfo.name} v${serverInfo.version}\u001B[24m\n`);
1572
1663
  if (!Is.stringValue(nodeOptions?.executionDirectory)) {
@@ -1624,7 +1715,8 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1624
1715
  }
1625
1716
  if (Is.arrayValue(options?.envFilenames)) {
1626
1717
  const output = dotenv.config({
1627
- path: options?.envFilenames
1718
+ path: options?.envFilenames,
1719
+ quiet: true
1628
1720
  });
1629
1721
  // We don't want to throw an error if the default environment file is not found.
1630
1722
  // Only if we have custom environment files.
@@ -1676,4 +1768,4 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1676
1768
  return { engineServerConfig, nodeEnvVars: envVars };
1677
1769
  }
1678
1770
 
1679
- export { NodeFeatures, bootstrap, bootstrapAttestationMethod, bootstrapAuth, bootstrapBlobEncryption, bootstrapImmutableProofMethod, bootstrapNodeIdentity, bootstrapNodeUser, buildConfiguration, buildEngineConfiguration, buildEngineServerConfiguration, fileExists, getExecutionDirectory, getFeatures, initialiseLocales, loadJsonFile, run, start };
1771
+ export { NodeFeatures, bootstrap, bootstrapAuth, bootstrapBlobEncryption, bootstrapImmutableProofMethod, bootstrapNodeIdentity, bootstrapNodeUser, bootstrapSynchronisedStorage, buildConfiguration, buildEngineConfiguration, buildEngineServerConfiguration, fileExists, getExecutionDirectory, getFeatures, initialiseLocales, loadJsonFile, run, start };