@twin.org/node-core 0.0.2-next.1 → 0.0.2-next.11

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,15 +1,17 @@
1
1
  import { PasswordHelper } from '@twin.org/api-auth-entity-storage-service';
2
- import { I18n, Is, Converter, RandomHelper, StringHelper, Coerce, Urn, GeneralError, ErrorHelper, EnvHelper } from '@twin.org/core';
2
+ import { I18n, Is, Coerce, Converter, RandomHelper, StringHelper, 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, TaskSchedulerComponentType, 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, RightsManagementPmpComponentType, RightsManagementPipComponentType, RightsManagementPxpComponentType, RightsManagementPdpComponentType, RightsManagementPepComponentType, RightsManagementPnpComponentType, RightsManagementPnapComponentType, RightsManagementPnrpComponentType, SynchronisedStorageComponentType, FederatedCatalogueComponentType, DataSpaceConnectorComponentType } 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';
8
9
  import { WalletConnectorFactory } from '@twin.org/wallet-models';
9
10
  import { readFile, stat } from 'node:fs/promises';
10
11
  import path from 'node:path';
12
+ import { PolicyNegotiationPointClient } from '@twin.org/rights-management-rest-client';
11
13
  import { addDefaultRestPaths, addDefaultSocketPaths, EngineServer } from '@twin.org/engine-server';
12
- import { InformationComponentType, RestRouteProcessorType, SocketRouteProcessorType, AuthenticationAdminComponentType, AuthenticationComponentType } from '@twin.org/engine-server-types';
14
+ import { ModuleHelper } from '@twin.org/modules';
13
15
  import * as dotenv from 'dotenv';
14
16
  import { Engine } from '@twin.org/engine';
15
17
  import { FileStateStorage } from '@twin.org/engine-core';
@@ -29,7 +31,11 @@ const NodeFeatures = {
29
31
  /**
30
32
  * NodeUser - generates a user for the node if not provided in config.
31
33
  */
32
- NodeUser: "node-user"
34
+ NodeUser: "node-user",
35
+ /**
36
+ * NodeWallet - generates a wallet for the node and funds it when there is a faucet available.
37
+ */
38
+ NodeWallet: "node-wallet"
33
39
  };
34
40
 
35
41
  // Copyright 2024 IOTA Stiftung.
@@ -71,13 +77,21 @@ async function fileExists(filename) {
71
77
  return false;
72
78
  }
73
79
  }
80
+ /**
81
+ * Load the text file.
82
+ * @param filename The filename of the text file to load.
83
+ * @returns The contents of the text file if it could not be loaded.
84
+ */
85
+ async function loadTextFile(filename) {
86
+ return readFile(filename, "utf8");
87
+ }
74
88
  /**
75
89
  * Load the JSON file.
76
90
  * @param filename The filename of the JSON file to load.
77
91
  * @returns The contents of the JSON file or null if it could not be loaded.
78
92
  */
79
93
  async function loadJsonFile(filename) {
80
- const content = await readFile(filename, "utf8");
94
+ const content = await loadTextFile(filename);
81
95
  return JSON.parse(content);
82
96
  }
83
97
  /**
@@ -116,8 +130,12 @@ async function bootstrap(engineCore, context, envVars) {
116
130
  await bootstrapNodeUser(engineCore, context, envVars, features);
117
131
  await bootstrapAuth(engineCore, context, envVars);
118
132
  await bootstrapBlobEncryption(engineCore, context, envVars);
119
- await bootstrapAttestationMethod(engineCore, context, envVars);
120
- await bootstrapImmutableProofMethod(engineCore, context, envVars);
133
+ await addVerificationMethod(engineCore, context, "attestation", envVars.attestationVerificationMethodId);
134
+ await addVerificationMethod(engineCore, context, "immutable proof", envVars.immutableProofVerificationMethodId);
135
+ if (Coerce.boolean(envVars.rightsManagementEnabled) ?? false) {
136
+ await addVerificationMethod(engineCore, context, "rights management", envVars.rightsManagementNegotiationMethodId);
137
+ }
138
+ await bootstrapSynchronisedStorage(engineCore, context, envVars);
121
139
  }
122
140
  /**
123
141
  * Bootstrap the node creating any necessary resources.
@@ -132,23 +150,21 @@ async function bootstrapNodeIdentity(engineCore, context, envVars, features) {
132
150
  // But we have a chicken and egg problem in that we can't create the identity
133
151
  // to store the mnemonic in the vault without an identity. We use a temporary identity
134
152
  // 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
- }
153
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
154
+ const vaultConnector = VaultConnectorFactory.get(defaultVaultConnectorType);
155
+ const workingIdentity = envVars.identity ??
156
+ context.state.nodeIdentity ??
157
+ `bootstrap-temp-${Converter.bytesToHex(RandomHelper.generate(16))}`;
158
+ await bootstrapMnemonic(engineCore, envVars, features, vaultConnector, workingIdentity);
159
+ const addresses = await bootstrapWallet(engineCore, envVars, features, workingIdentity);
160
+ const finalIdentity = await bootstrapIdentity(engineCore, envVars, features, workingIdentity);
161
+ await finaliseWallet(engineCore, envVars, features, finalIdentity, addresses);
162
+ await finaliseMnemonic(vaultConnector, workingIdentity, finalIdentity);
163
+ context.state.nodeIdentity = finalIdentity;
164
+ context.stateDirty = true;
165
+ engineCore.logInfo(I18n.formatMessage("node.nodeIdentity", {
166
+ identity: context.state.nodeIdentity
167
+ }));
152
168
  }
153
169
  }
154
170
  /**
@@ -160,12 +176,13 @@ async function bootstrapNodeIdentity(engineCore, context, envVars, features) {
160
176
  * @returns The addresses for the wallet.
161
177
  */
162
178
  async function bootstrapIdentity(engineCore, envVars, features, nodeIdentity) {
163
- const engineDefaultTypes = engineCore.getDefaultTypes();
179
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
164
180
  // Now create an identity for the node controlled by the address we just funded
165
- const identityConnector = IdentityConnectorFactory.get(engineDefaultTypes.identityConnector);
181
+ const identityConnector = IdentityConnectorFactory.get(defaultIdentityConnectorType);
166
182
  let identityDocument;
167
183
  try {
168
- const identityResolverConnector = IdentityResolverConnectorFactory.get(engineDefaultTypes.identityResolverConnector);
184
+ const defaultIdentityResolverConnectorType = engineCore.getRegisteredInstanceType("identityResolverConnector");
185
+ const identityResolverConnector = IdentityResolverConnectorFactory.get(defaultIdentityResolverConnectorType);
169
186
  identityDocument = await identityResolverConnector.resolveDocument(nodeIdentity);
170
187
  engineCore.logInfo(I18n.formatMessage("node.existingNodeIdentity", { identity: nodeIdentity }));
171
188
  }
@@ -175,7 +192,7 @@ async function bootstrapIdentity(engineCore, envVars, features, nodeIdentity) {
175
192
  identityDocument = await identityConnector.createDocument(nodeIdentity);
176
193
  engineCore.logInfo(I18n.formatMessage("node.createdNodeIdentity", { identity: identityDocument.id }));
177
194
  }
178
- if (engineDefaultTypes.identityConnector === IdentityConnectorType.Iota) {
195
+ if (defaultIdentityConnectorType.startsWith(IdentityConnectorType.Iota)) {
179
196
  const didUrn = Urn.fromValidString(identityDocument.id);
180
197
  const didParts = didUrn.parts();
181
198
  const objectId = didParts[3];
@@ -194,23 +211,26 @@ async function bootstrapIdentity(engineCore, envVars, features, nodeIdentity) {
194
211
  * @returns The addresses for the wallet.
195
212
  */
196
213
  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}`;
214
+ if (features.includes(NodeFeatures.NodeWallet)) {
215
+ const defaultWalletConnectorType = engineCore.getRegisteredInstanceType("walletConnector");
216
+ const walletConnector = WalletConnectorFactory.get(defaultWalletConnectorType);
217
+ const addresses = await walletConnector.getAddresses(nodeIdentity, 0, 0, 5);
218
+ const balance = await walletConnector.getBalance(nodeIdentity, addresses[0]);
219
+ if (balance === 0n) {
220
+ let address0 = addresses[0];
221
+ if (defaultWalletConnectorType.startsWith(WalletConnectorType.Iota)) {
222
+ address0 = `${envVars.iotaExplorerEndpoint}address/${address0}?network=${envVars.iotaNetwork}`;
223
+ }
224
+ engineCore.logInfo(I18n.formatMessage("node.fundingWallet", { address: address0 }));
225
+ // Add some funds to the wallet from the faucet
226
+ await walletConnector.ensureBalance(nodeIdentity, addresses[0], 1000000000n);
205
227
  }
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"));
228
+ else {
229
+ engineCore.logInfo(I18n.formatMessage("node.fundedWallet"));
230
+ }
231
+ return addresses;
212
232
  }
213
- return addresses;
233
+ return [];
214
234
  }
215
235
  /**
216
236
  * Bootstrap the identity for the node.
@@ -221,15 +241,17 @@ async function bootstrapWallet(engineCore, envVars, features, nodeIdentity) {
221
241
  * @param addresses The addresses for the wallet.
222
242
  */
223
243
  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);
244
+ if (features.includes(NodeFeatures.NodeWallet)) {
245
+ const defaultWalletConnectorType = engineCore.getRegisteredInstanceType("walletConnector");
246
+ // If we are using entity storage for wallet the identity associated with the
247
+ // address will be wrong, so fix it
248
+ if (defaultWalletConnectorType.startsWith(WalletConnectorType.EntityStorage)) {
249
+ const walletAddress = EntityStorageConnectorFactory.get(StringHelper.kebabCase("WalletAddress"));
250
+ const addr = await walletAddress.get(addresses[0]);
251
+ if (!Is.empty(addr)) {
252
+ addr.identity = finalIdentity;
253
+ await walletAddress.set(addr);
254
+ }
233
255
  }
234
256
  }
235
257
  }
@@ -291,8 +313,8 @@ async function finaliseMnemonic(vaultConnector, workingIdentity, finalIdentity)
291
313
  */
292
314
  async function bootstrapNodeUser(engineCore, context, envVars, features) {
293
315
  if (features.includes(NodeFeatures.NodeUser)) {
294
- const engineDefaultTypes = engineCore.getDefaultTypes();
295
- if (engineDefaultTypes.authenticationComponent === "authentication-entity-storage" &&
316
+ const defaultAuthenticationComponentType = engineCore.getRegisteredInstanceType("authenticationComponent");
317
+ if (defaultAuthenticationComponentType.startsWith(AuthenticationComponentType.EntityStorage) &&
296
318
  Is.stringValue(context.state.nodeIdentity)) {
297
319
  const authUserEntityStorage = EntityStorageConnectorFactory.get(StringHelper.kebabCase("AuthenticationUser"));
298
320
  const email = envVars.username ?? DEFAULT_NODE_USERNAME;
@@ -335,7 +357,8 @@ async function bootstrapNodeUser(engineCore, context, envVars, features) {
335
357
  }
336
358
  }
337
359
  // We have create a node user, now we need to create a profile for the user
338
- const identityProfileConnector = IdentityProfileConnectorFactory.get(engineDefaultTypes.identityProfileConnector);
360
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
361
+ const identityProfileConnector = IdentityProfileConnectorFactory.get(defaultIdentityConnectorType);
339
362
  if (identityProfileConnector) {
340
363
  let userProfile;
341
364
  try {
@@ -365,43 +388,6 @@ async function bootstrapNodeUser(engineCore, context, envVars, features) {
365
388
  }
366
389
  }
367
390
  }
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
391
  /**
406
392
  * Bootstrap the immutable proof verification methods.
407
393
  * @param engineCore The engine core for the node.
@@ -409,36 +395,7 @@ async function bootstrapAttestationMethod(engineCore, context, envVars, features
409
395
  * @param envVars The environment variables for the node.
410
396
  * @param features The features that are enabled on the node.
411
397
  */
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
- }
398
+ async function bootstrapImmutableProofMethod(engineCore, context, envVars, features) { }
442
399
  /**
443
400
  * Bootstrap the keys for blob encryption.
444
401
  * @param engineCore The engine core for the node.
@@ -449,18 +406,28 @@ async function bootstrapImmutableProofMethod(engineCore, context, envVars, featu
449
406
  async function bootstrapBlobEncryption(engineCore, context, envVars, features) {
450
407
  if ((Coerce.boolean(envVars.blobStorageEnableEncryption) ?? false) &&
451
408
  Is.stringValue(context.state.nodeIdentity)) {
452
- const engineDefaultTypes = engineCore.getDefaultTypes();
453
409
  // Create a new key for encrypting blobs
454
- const vaultConnector = VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
455
- const keyName = `${context.state.nodeIdentity}/${envVars.blobStorageEncryptionKey}`;
410
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
411
+ const vaultConnector = VaultConnectorFactory.get(defaultVaultConnectorType);
412
+ const keyName = `${context.state.nodeIdentity}/${envVars.blobStorageEncryptionKeyId}`;
456
413
  let existingKey;
457
414
  try {
458
415
  existingKey = await vaultConnector.getKey(keyName);
459
416
  }
460
417
  catch { }
461
418
  if (Is.empty(existingKey)) {
462
- engineCore.logInfo(I18n.formatMessage("node.creatingBlobEncryptionKey", { keyName }));
463
- await vaultConnector.createKey(keyName, VaultKeyType.ChaCha20Poly1305);
419
+ if (Is.stringBase64(envVars.blobStorageSymmetricEncryptionKey)) {
420
+ engineCore.logInfo(I18n.formatMessage("node.addingBlobEncryptionKey", { keyName }));
421
+ await vaultConnector.addKey(keyName, VaultKeyType.ChaCha20Poly1305, Converter.base64ToBytes(envVars.blobStorageSymmetricEncryptionKey));
422
+ }
423
+ else {
424
+ engineCore.logInfo(I18n.formatMessage("node.creatingBlobEncryptionKey", { keyName }));
425
+ const key = await vaultConnector.createKey(keyName, VaultKeyType.ChaCha20Poly1305);
426
+ engineCore.logInfo(I18n.formatMessage("node.createdBlobEncryptionKey", {
427
+ keyName,
428
+ keyValue: Converter.bytesToBase64(key)
429
+ }));
430
+ }
464
431
  }
465
432
  else {
466
433
  engineCore.logInfo(I18n.formatMessage("node.existingBlobEncryptionKey", { keyName }));
@@ -475,11 +442,13 @@ async function bootstrapBlobEncryption(engineCore, context, envVars, features) {
475
442
  * @param features The features that are enabled on the node.
476
443
  */
477
444
  async function bootstrapAuth(engineCore, context, envVars, features) {
478
- const engineDefaultTypes = engineCore.getDefaultTypes();
479
- if (engineDefaultTypes.authenticationComponent === "authentication-entity-storage" &&
445
+ const defaultAuthenticationComponentType = engineCore.getRegisteredInstanceTypeOptional("authenticationComponent");
446
+ if (Is.stringValue(defaultAuthenticationComponentType) &&
447
+ defaultAuthenticationComponentType.startsWith(AuthenticationComponentType.EntityStorage) &&
480
448
  Is.stringValue(context.state.nodeIdentity)) {
481
449
  // Create a new JWT signing key and a user login for the node
482
- const vaultConnector = VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
450
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
451
+ const vaultConnector = VaultConnectorFactory.get(defaultVaultConnectorType);
483
452
  const keyName = `${context.state.nodeIdentity}/${envVars.authSigningKeyId}`;
484
453
  let existingKey;
485
454
  try {
@@ -495,6 +464,76 @@ async function bootstrapAuth(engineCore, context, envVars, features) {
495
464
  }
496
465
  }
497
466
  }
467
+ /**
468
+ * Bootstrap the synchronised storage blob encryption and verification methods.
469
+ * @param engineCore The engine core for the node.
470
+ * @param context The context for the node.
471
+ * @param envVars The environment variables for the node.
472
+ * @param features The features that are enabled on the node.
473
+ */
474
+ async function bootstrapSynchronisedStorage(engineCore, context, envVars, features) {
475
+ if (Coerce.boolean(envVars.synchronisedStorageEnabled) ?? false) {
476
+ // Add the verification method to the identity if it doesn't exist
477
+ await addVerificationMethod(engineCore, context, "synchronised storage", envVars.synchronisedStorageVerificationMethodId);
478
+ // If this is a trusted node we need to add the blob encryption key pair
479
+ if (Is.stringValue(envVars.synchronisedStorageBlobStorageEncryptionKeyId) &&
480
+ Is.stringBase64(envVars.synchronisedStorageBlobStorageKey)) {
481
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
482
+ const vaultConnector = VaultConnectorFactory.get(defaultVaultConnectorType);
483
+ const keyName = envVars.synchronisedStorageBlobStorageEncryptionKeyId;
484
+ let existingKey;
485
+ try {
486
+ existingKey = await vaultConnector.getKey(keyName);
487
+ }
488
+ catch { }
489
+ if (Is.empty(existingKey)) {
490
+ engineCore.logInfo(I18n.formatMessage("node.addingSynchronisedStorageBlobEncryptionKey", { keyName }));
491
+ await vaultConnector.addKey(keyName, VaultKeyType.ChaCha20Poly1305, Converter.base64ToBytes(envVars.synchronisedStorageBlobStorageKey));
492
+ }
493
+ else {
494
+ engineCore.logInfo(I18n.formatMessage("node.existingSynchronisedStorageBlobEncryptionKey", { keyName }));
495
+ }
496
+ }
497
+ }
498
+ }
499
+ /**
500
+ * Add a verification method if it doesn't exist.
501
+ * @param engineCore The engine core for the node.
502
+ * @param context The context for the node.
503
+ * @param verificationMethodTitle The verification method title.
504
+ * @param verificationMethodId The verification method ID.
505
+ */
506
+ async function addVerificationMethod(engineCore, context, verificationMethodTitle, verificationMethodId) {
507
+ if (Is.stringValue(context.state.nodeIdentity) &&
508
+ Is.arrayValue(context.config.types.identityConnector) &&
509
+ Is.stringValue(verificationMethodId)) {
510
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
511
+ const identityConnector = IdentityConnectorFactory.get(defaultIdentityConnectorType);
512
+ const defaultIdentityResolverConnectorType = engineCore.getRegisteredInstanceType("identityResolverConnector");
513
+ const identityResolverConnector = IdentityResolverConnectorFactory.get(defaultIdentityResolverConnectorType);
514
+ const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
515
+ const fullMethodId = `${identityDocument.id}#${verificationMethodId}`;
516
+ let exists = false;
517
+ try {
518
+ DocumentHelper.getVerificationMethod(identityDocument, fullMethodId, "assertionMethod");
519
+ exists = true;
520
+ }
521
+ catch { }
522
+ if (!exists) {
523
+ engineCore.logInfo(I18n.formatMessage("node.addingVerificationMethod", {
524
+ title: verificationMethodTitle,
525
+ methodId: fullMethodId
526
+ }));
527
+ await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", verificationMethodId);
528
+ }
529
+ else {
530
+ engineCore.logInfo(I18n.formatMessage("node.existingVerificationMethod", {
531
+ title: verificationMethodTitle,
532
+ methodId: fullMethodId
533
+ }));
534
+ }
535
+ }
536
+ }
498
537
 
499
538
  // Copyright 2024 IOTA Stiftung.
500
539
  // SPDX-License-Identifier: Apache-2.0.
@@ -512,7 +551,10 @@ function buildEngineConfiguration(envVars) {
512
551
  envVars.attestationVerificationMethodId ??= "attestation-assertion";
513
552
  envVars.immutableProofVerificationMethodId ??= "immutable-proof-assertion";
514
553
  envVars.blobStorageEnableEncryption ??= "false";
515
- envVars.blobStorageEncryptionKey ??= "blob-encryption";
554
+ envVars.blobStorageEncryptionKeyId ??= "blob-encryption";
555
+ envVars.synchronisedStorageBlobStorageEncryptionKeyId ??= "synchronised-storage-blob-encryption";
556
+ envVars.synchronisedStorageVerificationMethodId ??= "synchronised-storage-assertion";
557
+ envVars.rightsManagementNegotiationMethodId ??= "policy-negotiation-assertion";
516
558
  const coreConfig = {
517
559
  debug: Coerce.boolean(envVars.debug) ?? false,
518
560
  types: {}
@@ -523,6 +565,7 @@ function buildEngineConfiguration(envVars) {
523
565
  configureDlt(coreConfig, envVars);
524
566
  configureLogging(coreConfig, envVars);
525
567
  configureBackgroundTask(coreConfig, envVars);
568
+ configureTaskScheduler(coreConfig, envVars);
526
569
  configureEventBus(coreConfig, envVars);
527
570
  configureTelemetry(coreConfig, envVars);
528
571
  configureMessaging(coreConfig, envVars);
@@ -538,9 +581,10 @@ function buildEngineConfiguration(envVars) {
538
581
  configureAuditableItemGraph(coreConfig);
539
582
  configureAuditableItemStream(coreConfig);
540
583
  configureDocumentManagement(coreConfig);
541
- configureFederatedCatalogue(coreConfig, envVars);
542
584
  configureRightsManagement(coreConfig, envVars);
543
- configureTaskScheduler(coreConfig, envVars);
585
+ configureSynchronisedStorage(coreConfig, envVars);
586
+ configureFederatedCatalogue(coreConfig, envVars);
587
+ configureDataSpaceConnector(coreConfig, envVars);
544
588
  return coreConfig;
545
589
  }
546
590
  /**
@@ -560,14 +604,13 @@ function getIotaConfig(coreConfig) {
560
604
  function configureEntityStorage(coreConfig, envVars) {
561
605
  coreConfig.types ??= {};
562
606
  coreConfig.types.entityStorageConnector ??= [];
563
- if ((Coerce.boolean(envVars.entityMemoryEnable) ?? false) ||
564
- envVars.entityStorageConnectorType === EntityStorageConnectorType.Memory) {
607
+ const entityStorageConnectorTypes = envVars.entityStorageConnectorType?.split(",") ?? [];
608
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.Memory)) {
565
609
  coreConfig.types.entityStorageConnector.push({
566
610
  type: EntityStorageConnectorType.Memory
567
611
  });
568
612
  }
569
- if ((Coerce.boolean(envVars.entityFileEnable) ?? false) ||
570
- envVars.entityStorageConnectorType === EntityStorageConnectorType.File) {
613
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.File)) {
571
614
  coreConfig.types.entityStorageConnector.push({
572
615
  type: EntityStorageConnectorType.File,
573
616
  options: {
@@ -576,7 +619,7 @@ function configureEntityStorage(coreConfig, envVars) {
576
619
  }
577
620
  });
578
621
  }
579
- if (Is.stringValue(envVars.awsDynamodbAccessKeyId)) {
622
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.AwsDynamoDb)) {
580
623
  coreConfig.types.entityStorageConnector.push({
581
624
  type: EntityStorageConnectorType.AwsDynamoDb,
582
625
  options: {
@@ -590,7 +633,7 @@ function configureEntityStorage(coreConfig, envVars) {
590
633
  }
591
634
  });
592
635
  }
593
- if (Is.stringValue(envVars.azureCosmosdbKey)) {
636
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.AzureCosmosDb)) {
594
637
  coreConfig.types.entityStorageConnector.push({
595
638
  type: EntityStorageConnectorType.AzureCosmosDb,
596
639
  options: {
@@ -604,7 +647,7 @@ function configureEntityStorage(coreConfig, envVars) {
604
647
  }
605
648
  });
606
649
  }
607
- if (Is.stringValue(envVars.gcpFirestoreCredentials)) {
650
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.GcpFirestoreDb)) {
608
651
  coreConfig.types.entityStorageConnector.push({
609
652
  type: EntityStorageConnectorType.GcpFirestoreDb,
610
653
  options: {
@@ -619,41 +662,27 @@ function configureEntityStorage(coreConfig, envVars) {
619
662
  }
620
663
  });
621
664
  }
622
- if (Is.stringValue(envVars.scylladbHosts)) {
665
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.ScyllaDb)) {
623
666
  coreConfig.types.entityStorageConnector.push({
624
667
  type: EntityStorageConnectorType.ScyllaDb,
625
668
  options: {
626
669
  config: {
627
- hosts: envVars.scylladbHosts.split(",") ?? "",
670
+ hosts: envVars.scylladbHosts?.split(",") ?? [],
628
671
  localDataCenter: envVars.scylladbLocalDataCenter ?? "",
629
- keyspace: envVars.scylladbKeyspace ?? ""
630
- },
631
- tablePrefix: envVars.entityStorageTablePrefix
632
- }
633
- });
634
- }
635
- if (Is.stringValue(envVars.mySqlHost)) {
636
- coreConfig.types.entityStorageConnector.push({
637
- type: EntityStorageConnectorType.MySqlDb,
638
- options: {
639
- config: {
640
- host: envVars.mySqlHost,
641
- port: envVars.mySqlPort ?? 3306,
642
- user: envVars.mySqlUser ?? "",
643
- password: envVars.mySqlPassword ?? "",
644
- database: envVars.mySqlDatabase ?? ""
672
+ keyspace: envVars.scylladbKeyspace ?? "",
673
+ port: Coerce.integer(envVars.scylladbPort)
645
674
  },
646
675
  tablePrefix: envVars.entityStorageTablePrefix
647
676
  }
648
677
  });
649
678
  }
650
- if (Is.stringValue(envVars.mySqlHost)) {
679
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.MySqlDb)) {
651
680
  coreConfig.types.entityStorageConnector.push({
652
681
  type: EntityStorageConnectorType.MySqlDb,
653
682
  options: {
654
683
  config: {
655
- host: envVars.mySqlHost,
656
- port: envVars.mySqlPort ?? 3306,
684
+ host: envVars.mySqlHost ?? "",
685
+ port: Coerce.integer(envVars.mySqlPort),
657
686
  user: envVars.mySqlUser ?? "",
658
687
  password: envVars.mySqlPassword ?? "",
659
688
  database: envVars.mySqlDatabase ?? ""
@@ -662,13 +691,13 @@ function configureEntityStorage(coreConfig, envVars) {
662
691
  }
663
692
  });
664
693
  }
665
- if (Is.stringValue(envVars.mongoDbHost)) {
694
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.MongoDb)) {
666
695
  coreConfig.types.entityStorageConnector.push({
667
696
  type: EntityStorageConnectorType.MongoDb,
668
697
  options: {
669
698
  config: {
670
- host: envVars.mongoDbHost,
671
- port: envVars.mongoDbPort,
699
+ host: envVars.mongoDbHost ?? "",
700
+ port: Coerce.integer(envVars.mongoDbPort),
672
701
  user: envVars.mongoDbUser ?? "",
673
702
  password: envVars.mongoDbPassword ?? "",
674
703
  database: envVars.mongoDbDatabase ?? ""
@@ -677,13 +706,13 @@ function configureEntityStorage(coreConfig, envVars) {
677
706
  }
678
707
  });
679
708
  }
680
- if (Is.stringValue(envVars.postgreSqlHost)) {
709
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.PostgreSql)) {
681
710
  coreConfig.types.entityStorageConnector.push({
682
711
  type: EntityStorageConnectorType.PostgreSql,
683
712
  options: {
684
713
  config: {
685
- host: envVars.postgreSqlHost,
686
- port: envVars.postgreSqlPort,
714
+ host: envVars.postgreSqlHost ?? "",
715
+ port: Coerce.integer(envVars.postgreSqlPort),
687
716
  user: envVars.postgreSqlUser ?? "",
688
717
  password: envVars.postgreSqlPassword ?? "",
689
718
  database: envVars.postgreSqlDatabase ?? ""
@@ -692,11 +721,21 @@ function configureEntityStorage(coreConfig, envVars) {
692
721
  }
693
722
  });
694
723
  }
695
- const defaultStorageConnector = envVars.entityStorageConnectorType;
696
- if (Is.stringValue(defaultStorageConnector)) {
724
+ const defaultEntityStorageConnectorType = envVars.entityStorageConnectorDefault ?? entityStorageConnectorTypes[0];
725
+ if (entityStorageConnectorTypes.includes(EntityStorageConnectorType.Synchronised)) {
726
+ // For synchronised storage we use the default connector as the one we wrap for real DB operations
727
+ coreConfig.types.entityStorageConnector.push({
728
+ type: EntityStorageConnectorType.Synchronised,
729
+ options: {
730
+ entityStorageConnectorType: defaultEntityStorageConnectorType
731
+ }
732
+ });
733
+ }
734
+ if (Is.arrayValue(entityStorageConnectorTypes)) {
697
735
  for (const config of coreConfig.types.entityStorageConnector) {
698
- if (config.type === defaultStorageConnector) {
736
+ if (config.type === defaultEntityStorageConnectorType) {
699
737
  config.isDefault = true;
738
+ break;
700
739
  }
701
740
  }
702
741
  }
@@ -708,14 +747,13 @@ function configureEntityStorage(coreConfig, envVars) {
708
747
  */
709
748
  function configureBlobStorage(coreConfig, envVars) {
710
749
  coreConfig.types.blobStorageConnector ??= [];
711
- if ((Coerce.boolean(envVars.blobMemoryEnable) ?? false) ||
712
- envVars.blobStorageConnectorType === BlobStorageConnectorType.Memory) {
750
+ const blobStorageConnectorTypes = envVars.blobStorageConnectorType?.split(",") ?? [];
751
+ if (blobStorageConnectorTypes.includes(BlobStorageConnectorType.Memory)) {
713
752
  coreConfig.types.blobStorageConnector.push({
714
753
  type: BlobStorageConnectorType.Memory
715
754
  });
716
755
  }
717
- if ((Coerce.boolean(envVars.blobFileEnable) ?? false) ||
718
- envVars.blobStorageConnectorType === BlobStorageConnectorType.File) {
756
+ if (blobStorageConnectorTypes.includes(BlobStorageConnectorType.File)) {
719
757
  coreConfig.types.blobStorageConnector.push({
720
758
  type: BlobStorageConnectorType.File,
721
759
  options: {
@@ -728,18 +766,18 @@ function configureBlobStorage(coreConfig, envVars) {
728
766
  }
729
767
  });
730
768
  }
731
- if (Is.stringValue(envVars.ipfsApiUrl)) {
769
+ if (blobStorageConnectorTypes.includes(BlobStorageConnectorType.Ipfs)) {
732
770
  coreConfig.types.blobStorageConnector.push({
733
771
  type: BlobStorageConnectorType.Ipfs,
734
772
  options: {
735
773
  config: {
736
- apiUrl: envVars.ipfsApiUrl,
774
+ apiUrl: envVars.ipfsApiUrl ?? "",
737
775
  bearerToken: envVars.ipfsBearerToken
738
776
  }
739
777
  }
740
778
  });
741
779
  }
742
- if (Is.stringValue(envVars.awsS3AccessKeyId)) {
780
+ if (blobStorageConnectorTypes.includes(BlobStorageConnectorType.AwsS3)) {
743
781
  coreConfig.types.blobStorageConnector.push({
744
782
  type: BlobStorageConnectorType.AwsS3,
745
783
  options: {
@@ -754,7 +792,7 @@ function configureBlobStorage(coreConfig, envVars) {
754
792
  }
755
793
  });
756
794
  }
757
- if (Is.stringValue(envVars.azureStorageAccountKey)) {
795
+ if (blobStorageConnectorTypes.includes(BlobStorageConnectorType.AzureStorage)) {
758
796
  coreConfig.types.blobStorageConnector.push({
759
797
  type: BlobStorageConnectorType.AzureStorage,
760
798
  options: {
@@ -768,7 +806,7 @@ function configureBlobStorage(coreConfig, envVars) {
768
806
  }
769
807
  });
770
808
  }
771
- if (Is.stringValue(envVars.gcpStorageCredentials)) {
809
+ if (blobStorageConnectorTypes.includes(BlobStorageConnectorType.GcpStorage)) {
772
810
  coreConfig.types.blobStorageConnector.push({
773
811
  type: BlobStorageConnectorType.GcpStorage,
774
812
  options: {
@@ -782,12 +820,20 @@ function configureBlobStorage(coreConfig, envVars) {
782
820
  }
783
821
  });
784
822
  }
785
- const defaultStorageConnectorType = envVars.blobStorageConnectorType;
786
- if (Is.stringValue(defaultStorageConnectorType)) {
823
+ if (Is.arrayValue(blobStorageConnectorTypes)) {
824
+ const defaultStorageConnectorType = envVars.blobStorageConnectorDefault ?? blobStorageConnectorTypes[0];
787
825
  for (const config of coreConfig.types.blobStorageConnector) {
788
826
  if (config.type === defaultStorageConnectorType) {
789
827
  config.isDefault = true;
790
828
  }
829
+ // If this blob storage connector is the one to use for public access
830
+ // then add it as a feature
831
+ if (Is.stringValue(envVars.blobStorageConnectorPublic) &&
832
+ config.type === envVars.blobStorageConnectorPublic) {
833
+ config.features ??= [];
834
+ config.features.push("public");
835
+ break;
836
+ }
791
837
  }
792
838
  }
793
839
  if (coreConfig.types.blobStorageConnector.length > 0) {
@@ -797,7 +843,7 @@ function configureBlobStorage(coreConfig, envVars) {
797
843
  options: {
798
844
  config: {
799
845
  vaultKeyId: (envVars.blobStorageEnableEncryption ?? false)
800
- ? envVars.blobStorageEncryptionKey
846
+ ? envVars.blobStorageEncryptionKeyId
801
847
  : undefined
802
848
  }
803
849
  }
@@ -1292,51 +1338,168 @@ function configureDocumentManagement(coreConfig, envVars) {
1292
1338
  }
1293
1339
  }
1294
1340
  /**
1295
- * Configures the federated catalogue.
1341
+ * Configures the rights management.
1296
1342
  * @param coreConfig The core config.
1297
1343
  * @param envVars The environment variables.
1298
1344
  */
1299
- function configureFederatedCatalogue(coreConfig, envVars) {
1300
- if (Is.arrayValue(coreConfig.types.identityResolverComponent)) {
1301
- coreConfig.types.federatedCatalogueComponent ??= [];
1302
- coreConfig.types.federatedCatalogueComponent.push({
1303
- type: FederatedCatalogueComponentType.Service,
1345
+ function configureRightsManagement(coreConfig, envVars) {
1346
+ if (Coerce.boolean(envVars.rightsManagementEnabled) ?? false) {
1347
+ coreConfig.types.rightsManagementPapComponent ??= [];
1348
+ coreConfig.types.rightsManagementPapComponent.push({
1349
+ type: RightsManagementPapComponentType.Service
1350
+ });
1351
+ coreConfig.types.rightsManagementPmpComponent ??= [];
1352
+ coreConfig.types.rightsManagementPmpComponent.push({
1353
+ type: RightsManagementPmpComponentType.Service
1354
+ });
1355
+ coreConfig.types.rightsManagementPipComponent ??= [];
1356
+ coreConfig.types.rightsManagementPipComponent.push({
1357
+ type: RightsManagementPipComponentType.Service,
1358
+ options: {
1359
+ informationModulesConfig: Is.arrayValue(envVars.rightsManagementInformationSources)
1360
+ ? envVars.rightsManagementInformationSources
1361
+ : undefined
1362
+ }
1363
+ });
1364
+ coreConfig.types.rightsManagementPxpComponent ??= [];
1365
+ coreConfig.types.rightsManagementPxpComponent.push({
1366
+ type: RightsManagementPxpComponentType.Service,
1367
+ options: {
1368
+ actionModulesConfig: Is.arrayValue(envVars.rightsManagementExecutionActions)
1369
+ ? envVars.rightsManagementExecutionActions
1370
+ : undefined
1371
+ }
1372
+ });
1373
+ coreConfig.types.rightsManagementPdpComponent ??= [];
1374
+ coreConfig.types.rightsManagementPdpComponent.push({
1375
+ type: RightsManagementPdpComponentType.Service
1376
+ });
1377
+ coreConfig.types.rightsManagementPepComponent ??= [];
1378
+ coreConfig.types.rightsManagementPepComponent.push({
1379
+ type: RightsManagementPepComponentType.Service,
1380
+ options: {
1381
+ processorModulesConfig: Is.arrayValue(envVars.rightsManagementEnforcementProcessors)
1382
+ ? envVars.rightsManagementEnforcementProcessors
1383
+ : undefined
1384
+ }
1385
+ });
1386
+ coreConfig.types.rightsManagementPnpComponent ??= [];
1387
+ coreConfig.types.rightsManagementPnpComponent.push({
1388
+ type: RightsManagementPnpComponentType.Service,
1389
+ options: {
1390
+ negotiatorModulesConfig: Is.arrayValue(envVars.rightsManagementNegotiators)
1391
+ ? envVars.rightsManagementNegotiators
1392
+ : undefined
1393
+ }
1394
+ });
1395
+ coreConfig.types.rightsManagementPnapComponent ??= [];
1396
+ coreConfig.types.rightsManagementPnapComponent.push({
1397
+ type: RightsManagementPnapComponentType.Service
1398
+ });
1399
+ coreConfig.types.rightsManagementPnrpComponent ??= [];
1400
+ coreConfig.types.rightsManagementPnrpComponent.push({
1401
+ type: RightsManagementPnrpComponentType.Service,
1304
1402
  options: {
1305
1403
  config: {
1306
- subResourceCacheTtlMs: Coerce.number(envVars.federatedCatalogueCacheTtlMs),
1307
- clearingHouseApproverList: Coerce.object(envVars.federatedCatalogueClearingHouseApproverList) ?? []
1404
+ negotiationMethodId: envVars.rightsManagementNegotiationMethodId ?? "",
1405
+ negotiationComponentCreator: async (url) => new PolicyNegotiationPointClient({ endpoint: url })
1308
1406
  }
1309
1407
  }
1310
1408
  });
1311
1409
  }
1312
1410
  }
1313
1411
  /**
1314
- * Configures the rights management.
1412
+ * Configures the task scheduler.
1315
1413
  * @param coreConfig The core config.
1316
1414
  * @param envVars The environment variables.
1317
1415
  */
1318
- function configureRightsManagement(coreConfig, envVars) {
1319
- if (Coerce.boolean(envVars.rightsManagementEnabled) ?? false) {
1320
- coreConfig.types.rightsManagementPapComponent ??= [];
1321
- coreConfig.types.rightsManagementPapComponent.push({
1322
- type: RightsManagementPapComponentType.Service
1416
+ function configureTaskScheduler(coreConfig, envVars) {
1417
+ if (Coerce.boolean(envVars.taskSchedulerEnabled) ?? false) {
1418
+ coreConfig.types.taskSchedulerComponent ??= [];
1419
+ coreConfig.types.taskSchedulerComponent.push({
1420
+ type: TaskSchedulerComponentType.Service
1421
+ });
1422
+ }
1423
+ }
1424
+ /**
1425
+ * Configures the synchronised storage.
1426
+ * @param coreConfig The core config.
1427
+ * @param envVars The environment variables.
1428
+ */
1429
+ function configureSynchronisedStorage(coreConfig, envVars) {
1430
+ if (Is.arrayValue(coreConfig.types.identityResolverComponent) &&
1431
+ (Coerce.boolean(envVars.synchronisedStorageEnabled) ?? false)) {
1432
+ // Check if the config provides a custom verifiable storage key id
1433
+ let verifiableStorageKeyId = Coerce.string(envVars.synchronisedStorageVerifiableStorageKeyId);
1434
+ if (!Is.stringValue(verifiableStorageKeyId)) {
1435
+ // No custom key so default to the network setting
1436
+ verifiableStorageKeyId = envVars.iotaNetwork;
1437
+ }
1438
+ coreConfig.types.synchronisedStorageComponent ??= [];
1439
+ coreConfig.types.synchronisedStorageComponent.push({
1440
+ type: SynchronisedStorageComponentType.Service,
1441
+ options: {
1442
+ config: {
1443
+ verifiableStorageKeyId: verifiableStorageKeyId ?? "",
1444
+ synchronisedStorageMethodId: envVars.synchronisedStorageVerificationMethodId,
1445
+ blobStorageEncryptionKeyId: envVars.synchronisedStorageBlobStorageEncryptionKeyId,
1446
+ entityUpdateIntervalMinutes: Coerce.number(envVars.synchronisedStorageEntityUpdateIntervalMinutes),
1447
+ consolidationIntervalMinutes: Coerce.number(envVars.synchronisedStorageConsolidationIntervalMinutes),
1448
+ consolidationBatchSize: Coerce.number(envVars.synchronisedStorageConsolidationBatchSize),
1449
+ maxConsolidations: Coerce.number(envVars.synchronisedStorageMaxConsolidations)
1450
+ }
1451
+ }
1323
1452
  });
1324
- coreConfig.types.rightsManagementComponent ??= [];
1325
- coreConfig.types.rightsManagementComponent.push({
1326
- type: RightsManagementComponentType.Service
1453
+ // If there is a trusted url set, we need to add a client
1454
+ // and give it a feature of trusted so that when the synchronised
1455
+ // storage is created it can pickup the correct component
1456
+ if (Is.stringValue(envVars.synchronisedStorageTrustedUrl)) {
1457
+ coreConfig.types.synchronisedStorageComponent.push({
1458
+ type: SynchronisedStorageComponentType.RestClient,
1459
+ options: {
1460
+ endpoint: envVars.synchronisedStorageTrustedUrl
1461
+ },
1462
+ features: ["trusted"]
1463
+ });
1464
+ }
1465
+ }
1466
+ }
1467
+ /**
1468
+ * Configures the federated catalogue.
1469
+ * @param coreConfig The core config.
1470
+ * @param envVars The environment variables.
1471
+ */
1472
+ function configureFederatedCatalogue(coreConfig, envVars) {
1473
+ if (Coerce.boolean(envVars.federatedCatalogueEnabled) ?? false) {
1474
+ coreConfig.types.federatedCatalogueComponent ??= [];
1475
+ coreConfig.types.federatedCatalogueComponent.push({
1476
+ type: FederatedCatalogueComponentType.Service,
1477
+ options: {
1478
+ config: {
1479
+ subResourceCacheTtlMs: Coerce.number(envVars.federatedCatalogueCacheTtlMs),
1480
+ clearingHouseApproverList: Coerce.object(envVars.federatedCatalogueClearingHouseApproverList) ?? []
1481
+ }
1482
+ }
1327
1483
  });
1328
1484
  }
1329
1485
  }
1330
1486
  /**
1331
- * Configures the task scheduler.
1487
+ * Configures the data space connector.
1332
1488
  * @param coreConfig The core config.
1333
1489
  * @param envVars The environment variables.
1334
1490
  */
1335
- function configureTaskScheduler(coreConfig, envVars) {
1336
- if (Coerce.boolean(envVars.taskSchedulerEnabled) ?? true) {
1337
- coreConfig.types.taskSchedulerComponent ??= [];
1338
- coreConfig.types.taskSchedulerComponent.push({
1339
- type: TaskSchedulerComponentType.Default
1491
+ function configureDataSpaceConnector(coreConfig, envVars) {
1492
+ if (Coerce.boolean(envVars.dataSpaceConnectorEnabled) ?? false) {
1493
+ coreConfig.types.dataSpaceConnectorComponent ??= [];
1494
+ coreConfig.types.dataSpaceConnectorComponent.push({
1495
+ type: DataSpaceConnectorComponentType.Service,
1496
+ options: {
1497
+ config: {
1498
+ dataSpaceConnectorAppDescriptors: Is.arrayValue(envVars.dataSpaceConnectorApps)
1499
+ ? envVars.dataSpaceConnectorApps
1500
+ : undefined
1501
+ }
1502
+ }
1340
1503
  });
1341
1504
  }
1342
1505
  }
@@ -1379,9 +1542,10 @@ function configureDlt(coreConfig, envVars) {
1379
1542
  * @param coreEngineConfig The core engine config.
1380
1543
  * @param serverInfo The server information.
1381
1544
  * @param openApiSpecPath The path to the open api spec.
1545
+ * @param favIconPath The path to the favicon.
1382
1546
  * @returns The the config for the core and the server.
1383
1547
  */
1384
- function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, openApiSpecPath) {
1548
+ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, openApiSpecPath, favIconPath) {
1385
1549
  envVars.authSigningKeyId ??= "auth-signing";
1386
1550
  const webServerOptions = {
1387
1551
  port: Coerce.number(envVars.port),
@@ -1408,7 +1572,8 @@ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, o
1408
1572
  options: {
1409
1573
  config: {
1410
1574
  serverInfo,
1411
- openApiSpecPath
1575
+ openApiSpecPath,
1576
+ favIconPath
1412
1577
  }
1413
1578
  }
1414
1579
  }
@@ -1525,16 +1690,19 @@ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, o
1525
1690
  * @returns The engine server.
1526
1691
  */
1527
1692
  async function start(nodeOptions, engineServerConfig, envVars) {
1528
- envVars.storageFileRoot ??= "";
1529
- if ((envVars.entityStorageConnectorType === EntityStorageConnectorType.File ||
1530
- envVars.blobStorageConnectorType === BlobStorageConnectorType.File ||
1693
+ const entityStorageConnectorType = envVars.entityStorageConnectorType?.split(",") ?? [];
1694
+ const blobStorageConnectorType = envVars.blobStorageConnectorType?.split(",") ?? [];
1695
+ // If the blob storage or entity storage is configured with file connectors
1696
+ // then we need to make sure the storageFileRoot is set
1697
+ if ((entityStorageConnectorType.includes(EntityStorageConnectorType.File) ||
1698
+ blobStorageConnectorType.includes(BlobStorageConnectorType.File) ||
1531
1699
  Is.empty(nodeOptions?.stateStorage)) &&
1532
1700
  !Is.stringValue(envVars.storageFileRoot)) {
1533
1701
  throw new GeneralError("node", "storageFileRootNotSet", {
1534
1702
  storageFileRoot: `${nodeOptions?.envPrefix ?? ""}_STORAGE_FILE_ROOT`
1535
1703
  });
1536
1704
  }
1537
- // Create the engine instance using file state storage and custom bootstrap.
1705
+ // Create the engine instance using file state storage unless one is configured in options
1538
1706
  const engine = new Engine({
1539
1707
  config: engineServerConfig,
1540
1708
  stateStorage: nodeOptions?.stateStorage ?? new FileStateStorage(envVars.stateFilename ?? ""),
@@ -1578,7 +1746,7 @@ async function run(nodeOptions) {
1578
1746
  nodeOptions ??= {};
1579
1747
  const serverInfo = {
1580
1748
  name: nodeOptions?.serverName ?? "TWIN Node Server",
1581
- version: nodeOptions?.serverVersion ?? "0.0.2-next.1" // x-release-please-version
1749
+ version: nodeOptions?.serverVersion ?? "0.0.2-next.11" // x-release-please-version
1582
1750
  };
1583
1751
  console.log(`\u001B[4mđŸŒŠī¸ ${serverInfo.name} v${serverInfo.version}\u001B[24m\n`);
1584
1752
  if (!Is.stringValue(nodeOptions?.executionDirectory)) {
@@ -1591,15 +1759,24 @@ async function run(nodeOptions) {
1591
1759
  console.info("Locales Directory:", nodeOptions.localesDirectory);
1592
1760
  await initialiseLocales(nodeOptions.localesDirectory);
1593
1761
  if (Is.empty(nodeOptions?.openApiSpecFile)) {
1594
- const specFile = path.resolve(path.join(nodeOptions.executionDirectory, "docs", "open-api", "spec.json"));
1762
+ const specFile = path.resolve(path.join(nodeOptions.executionDirectory ?? "", "docs", "open-api", "spec.json"));
1595
1763
  console.info("Default OpenAPI Spec File:", specFile);
1596
1764
  if (await fileExists(specFile)) {
1597
1765
  nodeOptions ??= {};
1598
1766
  nodeOptions.openApiSpecFile = specFile;
1599
1767
  }
1600
1768
  }
1769
+ if (Is.empty(nodeOptions?.favIconFile)) {
1770
+ const favIconFile = path.resolve(path.join(nodeOptions.executionDirectory ?? "", "static", "favicon.png"));
1771
+ console.info("Default Favicon File:", favIconFile);
1772
+ if (await fileExists(favIconFile)) {
1773
+ nodeOptions ??= {};
1774
+ nodeOptions.favIconFile = favIconFile;
1775
+ }
1776
+ }
1601
1777
  nodeOptions.envPrefix ??= "TWIN_NODE_";
1602
1778
  console.info("Environment Prefix:", nodeOptions.envPrefix);
1779
+ overrideModuleImport(nodeOptions.executionDirectory ?? "");
1603
1780
  const { engineServerConfig, nodeEnvVars: envVars } = await buildConfiguration(process.env, nodeOptions, serverInfo);
1604
1781
  console.info();
1605
1782
  const startResult = await start(nodeOptions, engineServerConfig, envVars);
@@ -1636,7 +1813,8 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1636
1813
  }
1637
1814
  if (Is.arrayValue(options?.envFilenames)) {
1638
1815
  const output = dotenv.config({
1639
- path: options?.envFilenames
1816
+ path: options?.envFilenames,
1817
+ quiet: true
1640
1818
  });
1641
1819
  // We don't want to throw an error if the default environment file is not found.
1642
1820
  // Only if we have custom environment files.
@@ -1651,12 +1829,18 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1651
1829
  // Expand any environment variables that use the @file: syntax
1652
1830
  const keys = Object.keys(envVars);
1653
1831
  for (const key of keys) {
1654
- if (Is.stringValue(envVars[key]) && envVars[key].startsWith("@file:")) {
1832
+ if (Is.stringValue(envVars[key]) &&
1833
+ (envVars[key].startsWith("@text:") || envVars[key].startsWith("@json:"))) {
1655
1834
  const filePath = envVars[key].slice(6).trim();
1656
1835
  const embeddedFile = path.resolve(path.join(options.executionDirectory ?? "", filePath));
1657
- console.info(`Expanding Environment Variable: ${key} from file: ${embeddedFile}`);
1658
- const fileContent = await loadJsonFile(embeddedFile);
1659
- envVars[key] = fileContent;
1836
+ if (envVars[key].startsWith("@text:")) {
1837
+ console.info(`Expanding Environment Variable: ${key} from text file: ${embeddedFile}`);
1838
+ envVars[key] = await loadTextFile(embeddedFile);
1839
+ }
1840
+ else if (envVars[key].startsWith("@json:")) {
1841
+ console.info(`Expanding Environment Variable: ${key} from JSON file: ${embeddedFile}`);
1842
+ envVars[key] = await loadJsonFile(embeddedFile);
1843
+ }
1660
1844
  }
1661
1845
  }
1662
1846
  // Extend the environment variables with any additional custom configuration.
@@ -1666,7 +1850,7 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1666
1850
  }
1667
1851
  // Build the engine configuration from the environment variables.
1668
1852
  const coreConfig = buildEngineConfiguration(envVars);
1669
- const engineServerConfig = buildEngineServerConfiguration(envVars, coreConfig, serverInfo, options?.openApiSpecFile);
1853
+ const engineServerConfig = buildEngineServerConfiguration(envVars, coreConfig, serverInfo, options?.openApiSpecFile, options?.favIconFile);
1670
1854
  // Merge any custom configuration provided in the options.
1671
1855
  if (Is.arrayValue(options?.configFilenames)) {
1672
1856
  for (const configFile of options.configFilenames) {
@@ -1687,5 +1871,39 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1687
1871
  }
1688
1872
  return { engineServerConfig, nodeEnvVars: envVars };
1689
1873
  }
1874
+ /**
1875
+ * Override module imports to use local files where possible.
1876
+ * @param executionDirectory The execution directory for resolving local module paths.
1877
+ */
1878
+ function overrideModuleImport(executionDirectory) {
1879
+ ModuleHelper.overrideImport(async (moduleName) => {
1880
+ // If the module path for example when dynamically loading
1881
+ // modules looks like a local file then we try to resolve
1882
+ // using the local file system
1883
+ const isLocal = ModuleHelper.isLocalModule(moduleName);
1884
+ if (isLocal) {
1885
+ // See if we can just resolve the filename locally
1886
+ let localFilename = path.resolve(moduleName);
1887
+ let exists = await fileExists(localFilename);
1888
+ if (!exists) {
1889
+ // Doesn't exist in the current directory, try the execution directory
1890
+ localFilename = path.resolve(executionDirectory, moduleName);
1891
+ exists = await fileExists(localFilename);
1892
+ }
1893
+ if (exists) {
1894
+ // If the module exists then we can load it, otherwise
1895
+ // we fallback to regular handling to see if that can import it
1896
+ return {
1897
+ module: await import(process.platform === "win32" ? `file://${localFilename}` : localFilename),
1898
+ useDefault: false
1899
+ };
1900
+ }
1901
+ }
1902
+ // The filename doesn't look like a local module, so just use default handling
1903
+ return {
1904
+ useDefault: true
1905
+ };
1906
+ });
1907
+ }
1690
1908
 
1691
- export { NodeFeatures, bootstrap, bootstrapAttestationMethod, bootstrapAuth, bootstrapBlobEncryption, bootstrapImmutableProofMethod, bootstrapNodeIdentity, bootstrapNodeUser, buildConfiguration, buildEngineConfiguration, buildEngineServerConfiguration, fileExists, getExecutionDirectory, getFeatures, initialiseLocales, loadJsonFile, run, start };
1909
+ export { NodeFeatures, bootstrap, bootstrapAuth, bootstrapBlobEncryption, bootstrapImmutableProofMethod, bootstrapNodeIdentity, bootstrapNodeUser, bootstrapSynchronisedStorage, buildConfiguration, buildEngineConfiguration, buildEngineServerConfiguration, fileExists, getExecutionDirectory, getFeatures, initialiseLocales, loadJsonFile, loadTextFile, overrideModuleImport, run, start };