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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@
3
3
  var apiAuthEntityStorageService = require('@twin.org/api-auth-entity-storage-service');
4
4
  var core = require('@twin.org/core');
5
5
  var crypto = require('@twin.org/crypto');
6
+ var engineServerTypes = require('@twin.org/engine-server-types');
6
7
  var engineTypes = require('@twin.org/engine-types');
7
8
  var entityStorageModels = require('@twin.org/entity-storage-models');
8
9
  var identityModels = require('@twin.org/identity-models');
@@ -10,8 +11,9 @@ var vaultModels = require('@twin.org/vault-models');
10
11
  var walletModels = require('@twin.org/wallet-models');
11
12
  var promises = require('node:fs/promises');
12
13
  var path = require('node:path');
14
+ var rightsManagementRestClient = require('@twin.org/rights-management-rest-client');
13
15
  var engineServer = require('@twin.org/engine-server');
14
- var engineServerTypes = require('@twin.org/engine-server-types');
16
+ var modules = require('@twin.org/modules');
15
17
  var dotenv = require('dotenv');
16
18
  var engine = require('@twin.org/engine');
17
19
  var engineCore = require('@twin.org/engine-core');
@@ -50,7 +52,11 @@ const NodeFeatures = {
50
52
  /**
51
53
  * NodeUser - generates a user for the node if not provided in config.
52
54
  */
53
- NodeUser: "node-user"
55
+ NodeUser: "node-user",
56
+ /**
57
+ * NodeWallet - generates a wallet for the node and funds it when there is a faucet available.
58
+ */
59
+ NodeWallet: "node-wallet"
54
60
  };
55
61
 
56
62
  // Copyright 2024 IOTA Stiftung.
@@ -92,13 +98,21 @@ async function fileExists(filename) {
92
98
  return false;
93
99
  }
94
100
  }
101
+ /**
102
+ * Load the text file.
103
+ * @param filename The filename of the text file to load.
104
+ * @returns The contents of the text file if it could not be loaded.
105
+ */
106
+ async function loadTextFile(filename) {
107
+ return promises.readFile(filename, "utf8");
108
+ }
95
109
  /**
96
110
  * Load the JSON file.
97
111
  * @param filename The filename of the JSON file to load.
98
112
  * @returns The contents of the JSON file or null if it could not be loaded.
99
113
  */
100
114
  async function loadJsonFile(filename) {
101
- const content = await promises.readFile(filename, "utf8");
115
+ const content = await loadTextFile(filename);
102
116
  return JSON.parse(content);
103
117
  }
104
118
  /**
@@ -137,8 +151,12 @@ async function bootstrap(engineCore, context, envVars) {
137
151
  await bootstrapNodeUser(engineCore, context, envVars, features);
138
152
  await bootstrapAuth(engineCore, context, envVars);
139
153
  await bootstrapBlobEncryption(engineCore, context, envVars);
140
- await bootstrapAttestationMethod(engineCore, context, envVars);
141
- await bootstrapImmutableProofMethod(engineCore, context, envVars);
154
+ await addVerificationMethod(engineCore, context, "attestation", envVars.attestationVerificationMethodId);
155
+ await addVerificationMethod(engineCore, context, "immutable proof", envVars.immutableProofVerificationMethodId);
156
+ if (core.Coerce.boolean(envVars.rightsManagementEnabled) ?? false) {
157
+ await addVerificationMethod(engineCore, context, "rights management", envVars.rightsManagementNegotiationMethodId);
158
+ }
159
+ await bootstrapSynchronisedStorage(engineCore, context, envVars);
142
160
  }
143
161
  /**
144
162
  * Bootstrap the node creating any necessary resources.
@@ -153,23 +171,21 @@ async function bootstrapNodeIdentity(engineCore, context, envVars, features) {
153
171
  // But we have a chicken and egg problem in that we can't create the identity
154
172
  // to store the mnemonic in the vault without an identity. We use a temporary identity
155
173
  // and then replace it with the new identity later in the process.
156
- const engineDefaultTypes = engineCore.getDefaultTypes();
157
- if (!core.Is.empty(engineDefaultTypes.vaultConnector)) {
158
- const vaultConnector = vaultModels.VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
159
- const workingIdentity = envVars.identity ??
160
- context.state.nodeIdentity ??
161
- `bootstrap-temp-${core.Converter.bytesToHex(core.RandomHelper.generate(16))}`;
162
- await bootstrapMnemonic(engineCore, envVars, features, vaultConnector, workingIdentity);
163
- const addresses = await bootstrapWallet(engineCore, envVars, features, workingIdentity);
164
- const finalIdentity = await bootstrapIdentity(engineCore, envVars, features, workingIdentity);
165
- await finaliseWallet(engineCore, envVars, features, finalIdentity, addresses);
166
- await finaliseMnemonic(vaultConnector, workingIdentity, finalIdentity);
167
- context.state.nodeIdentity = finalIdentity;
168
- context.stateDirty = true;
169
- engineCore.logInfo(core.I18n.formatMessage("node.nodeIdentity", {
170
- identity: context.state.nodeIdentity
171
- }));
172
- }
174
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
175
+ const vaultConnector = vaultModels.VaultConnectorFactory.get(defaultVaultConnectorType);
176
+ const workingIdentity = envVars.identity ??
177
+ context.state.nodeIdentity ??
178
+ `bootstrap-temp-${core.Converter.bytesToHex(core.RandomHelper.generate(16))}`;
179
+ await bootstrapMnemonic(engineCore, envVars, features, vaultConnector, workingIdentity);
180
+ const addresses = await bootstrapWallet(engineCore, envVars, features, workingIdentity);
181
+ const finalIdentity = await bootstrapIdentity(engineCore, envVars, features, workingIdentity);
182
+ await finaliseWallet(engineCore, envVars, features, finalIdentity, addresses);
183
+ await finaliseMnemonic(vaultConnector, workingIdentity, finalIdentity);
184
+ context.state.nodeIdentity = finalIdentity;
185
+ context.stateDirty = true;
186
+ engineCore.logInfo(core.I18n.formatMessage("node.nodeIdentity", {
187
+ identity: context.state.nodeIdentity
188
+ }));
173
189
  }
174
190
  }
175
191
  /**
@@ -181,12 +197,13 @@ async function bootstrapNodeIdentity(engineCore, context, envVars, features) {
181
197
  * @returns The addresses for the wallet.
182
198
  */
183
199
  async function bootstrapIdentity(engineCore, envVars, features, nodeIdentity) {
184
- const engineDefaultTypes = engineCore.getDefaultTypes();
200
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
185
201
  // Now create an identity for the node controlled by the address we just funded
186
- const identityConnector = identityModels.IdentityConnectorFactory.get(engineDefaultTypes.identityConnector);
202
+ const identityConnector = identityModels.IdentityConnectorFactory.get(defaultIdentityConnectorType);
187
203
  let identityDocument;
188
204
  try {
189
- const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(engineDefaultTypes.identityResolverConnector);
205
+ const defaultIdentityResolverConnectorType = engineCore.getRegisteredInstanceType("identityResolverConnector");
206
+ const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(defaultIdentityResolverConnectorType);
190
207
  identityDocument = await identityResolverConnector.resolveDocument(nodeIdentity);
191
208
  engineCore.logInfo(core.I18n.formatMessage("node.existingNodeIdentity", { identity: nodeIdentity }));
192
209
  }
@@ -196,7 +213,7 @@ async function bootstrapIdentity(engineCore, envVars, features, nodeIdentity) {
196
213
  identityDocument = await identityConnector.createDocument(nodeIdentity);
197
214
  engineCore.logInfo(core.I18n.formatMessage("node.createdNodeIdentity", { identity: identityDocument.id }));
198
215
  }
199
- if (engineDefaultTypes.identityConnector === engineTypes.IdentityConnectorType.Iota) {
216
+ if (defaultIdentityConnectorType.startsWith(engineTypes.IdentityConnectorType.Iota)) {
200
217
  const didUrn = core.Urn.fromValidString(identityDocument.id);
201
218
  const didParts = didUrn.parts();
202
219
  const objectId = didParts[3];
@@ -215,23 +232,26 @@ async function bootstrapIdentity(engineCore, envVars, features, nodeIdentity) {
215
232
  * @returns The addresses for the wallet.
216
233
  */
217
234
  async function bootstrapWallet(engineCore, envVars, features, nodeIdentity) {
218
- const engineDefaultTypes = engineCore.getDefaultTypes();
219
- const walletConnector = walletModels.WalletConnectorFactory.get(engineDefaultTypes.walletConnector);
220
- const addresses = await walletConnector.getAddresses(nodeIdentity, 0, 0, 5);
221
- const balance = await walletConnector.getBalance(nodeIdentity, addresses[0]);
222
- if (balance === 0n) {
223
- let address0 = addresses[0];
224
- if (engineDefaultTypes.walletConnector === engineTypes.WalletConnectorType.Iota) {
225
- address0 = `${envVars.iotaExplorerEndpoint}address/${address0}?network=${envVars.iotaNetwork}`;
235
+ if (features.includes(NodeFeatures.NodeWallet)) {
236
+ const defaultWalletConnectorType = engineCore.getRegisteredInstanceType("walletConnector");
237
+ const walletConnector = walletModels.WalletConnectorFactory.get(defaultWalletConnectorType);
238
+ const addresses = await walletConnector.getAddresses(nodeIdentity, 0, 0, 5);
239
+ const balance = await walletConnector.getBalance(nodeIdentity, addresses[0]);
240
+ if (balance === 0n) {
241
+ let address0 = addresses[0];
242
+ if (defaultWalletConnectorType.startsWith(engineTypes.WalletConnectorType.Iota)) {
243
+ address0 = `${envVars.iotaExplorerEndpoint}address/${address0}?network=${envVars.iotaNetwork}`;
244
+ }
245
+ engineCore.logInfo(core.I18n.formatMessage("node.fundingWallet", { address: address0 }));
246
+ // Add some funds to the wallet from the faucet
247
+ await walletConnector.ensureBalance(nodeIdentity, addresses[0], 1000000000n);
226
248
  }
227
- engineCore.logInfo(core.I18n.formatMessage("node.fundingWallet", { address: address0 }));
228
- // Add some funds to the wallet from the faucet
229
- await walletConnector.ensureBalance(nodeIdentity, addresses[0], 1000000000n);
230
- }
231
- else {
232
- engineCore.logInfo(core.I18n.formatMessage("node.fundedWallet"));
249
+ else {
250
+ engineCore.logInfo(core.I18n.formatMessage("node.fundedWallet"));
251
+ }
252
+ return addresses;
233
253
  }
234
- return addresses;
254
+ return [];
235
255
  }
236
256
  /**
237
257
  * Bootstrap the identity for the node.
@@ -242,15 +262,17 @@ async function bootstrapWallet(engineCore, envVars, features, nodeIdentity) {
242
262
  * @param addresses The addresses for the wallet.
243
263
  */
244
264
  async function finaliseWallet(engineCore, envVars, features, finalIdentity, addresses) {
245
- const engineDefaultTypes = engineCore.getDefaultTypes();
246
- // If we are using entity storage for wallet the identity associated with the
247
- // address will be wrong, so fix it
248
- if (engineDefaultTypes.walletConnector === "entity-storage") {
249
- const walletAddress = entityStorageModels.EntityStorageConnectorFactory.get(core.StringHelper.kebabCase("WalletAddress"));
250
- const addr = await walletAddress.get(addresses[0]);
251
- if (!core.Is.empty(addr)) {
252
- addr.identity = finalIdentity;
253
- await walletAddress.set(addr);
265
+ if (features.includes(NodeFeatures.NodeWallet)) {
266
+ const defaultWalletConnectorType = engineCore.getRegisteredInstanceType("walletConnector");
267
+ // If we are using entity storage for wallet the identity associated with the
268
+ // address will be wrong, so fix it
269
+ if (defaultWalletConnectorType.startsWith(engineTypes.WalletConnectorType.EntityStorage)) {
270
+ const walletAddress = entityStorageModels.EntityStorageConnectorFactory.get(core.StringHelper.kebabCase("WalletAddress"));
271
+ const addr = await walletAddress.get(addresses[0]);
272
+ if (!core.Is.empty(addr)) {
273
+ addr.identity = finalIdentity;
274
+ await walletAddress.set(addr);
275
+ }
254
276
  }
255
277
  }
256
278
  }
@@ -312,8 +334,8 @@ async function finaliseMnemonic(vaultConnector, workingIdentity, finalIdentity)
312
334
  */
313
335
  async function bootstrapNodeUser(engineCore, context, envVars, features) {
314
336
  if (features.includes(NodeFeatures.NodeUser)) {
315
- const engineDefaultTypes = engineCore.getDefaultTypes();
316
- if (engineDefaultTypes.authenticationComponent === "authentication-entity-storage" &&
337
+ const defaultAuthenticationComponentType = engineCore.getRegisteredInstanceType("authenticationComponent");
338
+ if (defaultAuthenticationComponentType.startsWith(engineServerTypes.AuthenticationComponentType.EntityStorage) &&
317
339
  core.Is.stringValue(context.state.nodeIdentity)) {
318
340
  const authUserEntityStorage = entityStorageModels.EntityStorageConnectorFactory.get(core.StringHelper.kebabCase("AuthenticationUser"));
319
341
  const email = envVars.username ?? DEFAULT_NODE_USERNAME;
@@ -356,7 +378,8 @@ async function bootstrapNodeUser(engineCore, context, envVars, features) {
356
378
  }
357
379
  }
358
380
  // We have create a node user, now we need to create a profile for the user
359
- const identityProfileConnector = identityModels.IdentityProfileConnectorFactory.get(engineDefaultTypes.identityProfileConnector);
381
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
382
+ const identityProfileConnector = identityModels.IdentityProfileConnectorFactory.get(defaultIdentityConnectorType);
360
383
  if (identityProfileConnector) {
361
384
  let userProfile;
362
385
  try {
@@ -386,43 +409,6 @@ async function bootstrapNodeUser(engineCore, context, envVars, features) {
386
409
  }
387
410
  }
388
411
  }
389
- /**
390
- * Bootstrap the attestation verification methods.
391
- * @param engineCore The engine core for the node.
392
- * @param context The context for the node.
393
- * @param envVars The environment variables for the node.
394
- * @param features The features that are enabled on the node.
395
- */
396
- async function bootstrapAttestationMethod(engineCore, context, envVars, features) {
397
- if (core.Is.stringValue(context.state.nodeIdentity) &&
398
- core.Is.arrayValue(context.config.types.identityConnector) &&
399
- core.Is.stringValue(envVars.attestationVerificationMethodId)) {
400
- const engineDefaultTypes = engineCore.getDefaultTypes();
401
- const identityConnector = identityModels.IdentityConnectorFactory.get(engineDefaultTypes.identityConnector);
402
- const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(engineDefaultTypes.identityResolverConnector);
403
- const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
404
- const fullMethodId = `${identityDocument.id}#${envVars.attestationVerificationMethodId}`;
405
- let createVm = true;
406
- try {
407
- identityModels.DocumentHelper.getVerificationMethod(identityDocument, fullMethodId, "assertionMethod");
408
- createVm = false;
409
- }
410
- catch { }
411
- if (createVm) {
412
- // Add attestation verification method to DID, the correct node context is now in place
413
- // so the keys for the verification method will be stored correctly
414
- engineCore.logInfo(core.I18n.formatMessage("node.addingAttestation", {
415
- methodId: fullMethodId
416
- }));
417
- await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", envVars.attestationVerificationMethodId);
418
- }
419
- else {
420
- engineCore.logInfo(core.I18n.formatMessage("node.existingAttestation", {
421
- methodId: fullMethodId
422
- }));
423
- }
424
- }
425
- }
426
412
  /**
427
413
  * Bootstrap the immutable proof verification methods.
428
414
  * @param engineCore The engine core for the node.
@@ -430,36 +416,7 @@ async function bootstrapAttestationMethod(engineCore, context, envVars, features
430
416
  * @param envVars The environment variables for the node.
431
417
  * @param features The features that are enabled on the node.
432
418
  */
433
- async function bootstrapImmutableProofMethod(engineCore, context, envVars, features) {
434
- const engineDefaultTypes = engineCore.getDefaultTypes();
435
- if (core.Is.stringValue(context.state.nodeIdentity) &&
436
- core.Is.arrayValue(context.config.types.identityConnector) &&
437
- core.Is.stringValue(envVars.immutableProofVerificationMethodId)) {
438
- const identityConnector = identityModels.IdentityConnectorFactory.get(engineDefaultTypes.identityConnector);
439
- const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(engineDefaultTypes.identityResolverConnector);
440
- const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
441
- const fullMethodId = `${identityDocument.id}#${envVars.immutableProofVerificationMethodId}`;
442
- let createVm = true;
443
- try {
444
- identityModels.DocumentHelper.getVerificationMethod(identityDocument, fullMethodId, "assertionMethod");
445
- createVm = false;
446
- }
447
- catch { }
448
- if (createVm) {
449
- // Add AIG verification method to DID, the correct node context is now in place
450
- // so the keys for the verification method will be stored correctly
451
- engineCore.logInfo(core.I18n.formatMessage("node.addingImmutableProof", {
452
- methodId: fullMethodId
453
- }));
454
- await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", envVars.immutableProofVerificationMethodId);
455
- }
456
- else {
457
- engineCore.logInfo(core.I18n.formatMessage("node.existingImmutableProof", {
458
- methodId: fullMethodId
459
- }));
460
- }
461
- }
462
- }
419
+ async function bootstrapImmutableProofMethod(engineCore, context, envVars, features) { }
463
420
  /**
464
421
  * Bootstrap the keys for blob encryption.
465
422
  * @param engineCore The engine core for the node.
@@ -470,18 +427,28 @@ async function bootstrapImmutableProofMethod(engineCore, context, envVars, featu
470
427
  async function bootstrapBlobEncryption(engineCore, context, envVars, features) {
471
428
  if ((core.Coerce.boolean(envVars.blobStorageEnableEncryption) ?? false) &&
472
429
  core.Is.stringValue(context.state.nodeIdentity)) {
473
- const engineDefaultTypes = engineCore.getDefaultTypes();
474
430
  // Create a new key for encrypting blobs
475
- const vaultConnector = vaultModels.VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
476
- const keyName = `${context.state.nodeIdentity}/${envVars.blobStorageEncryptionKey}`;
431
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
432
+ const vaultConnector = vaultModels.VaultConnectorFactory.get(defaultVaultConnectorType);
433
+ const keyName = `${context.state.nodeIdentity}/${envVars.blobStorageEncryptionKeyId}`;
477
434
  let existingKey;
478
435
  try {
479
436
  existingKey = await vaultConnector.getKey(keyName);
480
437
  }
481
438
  catch { }
482
439
  if (core.Is.empty(existingKey)) {
483
- engineCore.logInfo(core.I18n.formatMessage("node.creatingBlobEncryptionKey", { keyName }));
484
- await vaultConnector.createKey(keyName, vaultModels.VaultKeyType.ChaCha20Poly1305);
440
+ if (core.Is.stringBase64(envVars.blobStorageSymmetricEncryptionKey)) {
441
+ engineCore.logInfo(core.I18n.formatMessage("node.addingBlobEncryptionKey", { keyName }));
442
+ await vaultConnector.addKey(keyName, vaultModels.VaultKeyType.ChaCha20Poly1305, core.Converter.base64ToBytes(envVars.blobStorageSymmetricEncryptionKey));
443
+ }
444
+ else {
445
+ engineCore.logInfo(core.I18n.formatMessage("node.creatingBlobEncryptionKey", { keyName }));
446
+ const key = await vaultConnector.createKey(keyName, vaultModels.VaultKeyType.ChaCha20Poly1305);
447
+ engineCore.logInfo(core.I18n.formatMessage("node.createdBlobEncryptionKey", {
448
+ keyName,
449
+ keyValue: core.Converter.bytesToBase64(key)
450
+ }));
451
+ }
485
452
  }
486
453
  else {
487
454
  engineCore.logInfo(core.I18n.formatMessage("node.existingBlobEncryptionKey", { keyName }));
@@ -496,11 +463,13 @@ async function bootstrapBlobEncryption(engineCore, context, envVars, features) {
496
463
  * @param features The features that are enabled on the node.
497
464
  */
498
465
  async function bootstrapAuth(engineCore, context, envVars, features) {
499
- const engineDefaultTypes = engineCore.getDefaultTypes();
500
- if (engineDefaultTypes.authenticationComponent === "authentication-entity-storage" &&
466
+ const defaultAuthenticationComponentType = engineCore.getRegisteredInstanceTypeOptional("authenticationComponent");
467
+ if (core.Is.stringValue(defaultAuthenticationComponentType) &&
468
+ defaultAuthenticationComponentType.startsWith(engineServerTypes.AuthenticationComponentType.EntityStorage) &&
501
469
  core.Is.stringValue(context.state.nodeIdentity)) {
502
470
  // Create a new JWT signing key and a user login for the node
503
- const vaultConnector = vaultModels.VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
471
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
472
+ const vaultConnector = vaultModels.VaultConnectorFactory.get(defaultVaultConnectorType);
504
473
  const keyName = `${context.state.nodeIdentity}/${envVars.authSigningKeyId}`;
505
474
  let existingKey;
506
475
  try {
@@ -516,6 +485,76 @@ async function bootstrapAuth(engineCore, context, envVars, features) {
516
485
  }
517
486
  }
518
487
  }
488
+ /**
489
+ * Bootstrap the synchronised storage blob encryption and verification methods.
490
+ * @param engineCore The engine core for the node.
491
+ * @param context The context for the node.
492
+ * @param envVars The environment variables for the node.
493
+ * @param features The features that are enabled on the node.
494
+ */
495
+ async function bootstrapSynchronisedStorage(engineCore, context, envVars, features) {
496
+ if (core.Coerce.boolean(envVars.synchronisedStorageEnabled) ?? false) {
497
+ // Add the verification method to the identity if it doesn't exist
498
+ await addVerificationMethod(engineCore, context, "synchronised storage", envVars.synchronisedStorageVerificationMethodId);
499
+ // If this is a trusted node we need to add the blob encryption key pair
500
+ if (core.Is.stringValue(envVars.synchronisedStorageBlobStorageEncryptionKeyId) &&
501
+ core.Is.stringBase64(envVars.synchronisedStorageBlobStorageKey)) {
502
+ const defaultVaultConnectorType = engineCore.getRegisteredInstanceType("vaultConnector");
503
+ const vaultConnector = vaultModels.VaultConnectorFactory.get(defaultVaultConnectorType);
504
+ const keyName = envVars.synchronisedStorageBlobStorageEncryptionKeyId;
505
+ let existingKey;
506
+ try {
507
+ existingKey = await vaultConnector.getKey(keyName);
508
+ }
509
+ catch { }
510
+ if (core.Is.empty(existingKey)) {
511
+ engineCore.logInfo(core.I18n.formatMessage("node.addingSynchronisedStorageBlobEncryptionKey", { keyName }));
512
+ await vaultConnector.addKey(keyName, vaultModels.VaultKeyType.ChaCha20Poly1305, core.Converter.base64ToBytes(envVars.synchronisedStorageBlobStorageKey));
513
+ }
514
+ else {
515
+ engineCore.logInfo(core.I18n.formatMessage("node.existingSynchronisedStorageBlobEncryptionKey", { keyName }));
516
+ }
517
+ }
518
+ }
519
+ }
520
+ /**
521
+ * Add a verification method if it doesn't exist.
522
+ * @param engineCore The engine core for the node.
523
+ * @param context The context for the node.
524
+ * @param verificationMethodTitle The verification method title.
525
+ * @param verificationMethodId The verification method ID.
526
+ */
527
+ async function addVerificationMethod(engineCore, context, verificationMethodTitle, verificationMethodId) {
528
+ if (core.Is.stringValue(context.state.nodeIdentity) &&
529
+ core.Is.arrayValue(context.config.types.identityConnector) &&
530
+ core.Is.stringValue(verificationMethodId)) {
531
+ const defaultIdentityConnectorType = engineCore.getRegisteredInstanceType("identityConnector");
532
+ const identityConnector = identityModels.IdentityConnectorFactory.get(defaultIdentityConnectorType);
533
+ const defaultIdentityResolverConnectorType = engineCore.getRegisteredInstanceType("identityResolverConnector");
534
+ const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(defaultIdentityResolverConnectorType);
535
+ const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
536
+ const fullMethodId = `${identityDocument.id}#${verificationMethodId}`;
537
+ let exists = false;
538
+ try {
539
+ identityModels.DocumentHelper.getVerificationMethod(identityDocument, fullMethodId, "assertionMethod");
540
+ exists = true;
541
+ }
542
+ catch { }
543
+ if (!exists) {
544
+ engineCore.logInfo(core.I18n.formatMessage("node.addingVerificationMethod", {
545
+ title: verificationMethodTitle,
546
+ methodId: fullMethodId
547
+ }));
548
+ await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", verificationMethodId);
549
+ }
550
+ else {
551
+ engineCore.logInfo(core.I18n.formatMessage("node.existingVerificationMethod", {
552
+ title: verificationMethodTitle,
553
+ methodId: fullMethodId
554
+ }));
555
+ }
556
+ }
557
+ }
519
558
 
520
559
  // Copyright 2024 IOTA Stiftung.
521
560
  // SPDX-License-Identifier: Apache-2.0.
@@ -533,7 +572,10 @@ function buildEngineConfiguration(envVars) {
533
572
  envVars.attestationVerificationMethodId ??= "attestation-assertion";
534
573
  envVars.immutableProofVerificationMethodId ??= "immutable-proof-assertion";
535
574
  envVars.blobStorageEnableEncryption ??= "false";
536
- envVars.blobStorageEncryptionKey ??= "blob-encryption";
575
+ envVars.blobStorageEncryptionKeyId ??= "blob-encryption";
576
+ envVars.synchronisedStorageBlobStorageEncryptionKeyId ??= "synchronised-storage-blob-encryption";
577
+ envVars.synchronisedStorageVerificationMethodId ??= "synchronised-storage-assertion";
578
+ envVars.rightsManagementNegotiationMethodId ??= "policy-negotiation-assertion";
537
579
  const coreConfig = {
538
580
  debug: core.Coerce.boolean(envVars.debug) ?? false,
539
581
  types: {}
@@ -544,6 +586,7 @@ function buildEngineConfiguration(envVars) {
544
586
  configureDlt(coreConfig, envVars);
545
587
  configureLogging(coreConfig, envVars);
546
588
  configureBackgroundTask(coreConfig, envVars);
589
+ configureTaskScheduler(coreConfig, envVars);
547
590
  configureEventBus(coreConfig, envVars);
548
591
  configureTelemetry(coreConfig, envVars);
549
592
  configureMessaging(coreConfig, envVars);
@@ -559,9 +602,10 @@ function buildEngineConfiguration(envVars) {
559
602
  configureAuditableItemGraph(coreConfig);
560
603
  configureAuditableItemStream(coreConfig);
561
604
  configureDocumentManagement(coreConfig);
562
- configureFederatedCatalogue(coreConfig, envVars);
563
605
  configureRightsManagement(coreConfig, envVars);
564
- configureTaskScheduler(coreConfig, envVars);
606
+ configureSynchronisedStorage(coreConfig, envVars);
607
+ configureFederatedCatalogue(coreConfig, envVars);
608
+ configureDataSpaceConnector(coreConfig, envVars);
565
609
  return coreConfig;
566
610
  }
567
611
  /**
@@ -581,14 +625,13 @@ function getIotaConfig(coreConfig) {
581
625
  function configureEntityStorage(coreConfig, envVars) {
582
626
  coreConfig.types ??= {};
583
627
  coreConfig.types.entityStorageConnector ??= [];
584
- if ((core.Coerce.boolean(envVars.entityMemoryEnable) ?? false) ||
585
- envVars.entityStorageConnectorType === engineTypes.EntityStorageConnectorType.Memory) {
628
+ const entityStorageConnectorTypes = envVars.entityStorageConnectorType?.split(",") ?? [];
629
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.Memory)) {
586
630
  coreConfig.types.entityStorageConnector.push({
587
631
  type: engineTypes.EntityStorageConnectorType.Memory
588
632
  });
589
633
  }
590
- if ((core.Coerce.boolean(envVars.entityFileEnable) ?? false) ||
591
- envVars.entityStorageConnectorType === engineTypes.EntityStorageConnectorType.File) {
634
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.File)) {
592
635
  coreConfig.types.entityStorageConnector.push({
593
636
  type: engineTypes.EntityStorageConnectorType.File,
594
637
  options: {
@@ -597,7 +640,7 @@ function configureEntityStorage(coreConfig, envVars) {
597
640
  }
598
641
  });
599
642
  }
600
- if (core.Is.stringValue(envVars.awsDynamodbAccessKeyId)) {
643
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.AwsDynamoDb)) {
601
644
  coreConfig.types.entityStorageConnector.push({
602
645
  type: engineTypes.EntityStorageConnectorType.AwsDynamoDb,
603
646
  options: {
@@ -611,7 +654,7 @@ function configureEntityStorage(coreConfig, envVars) {
611
654
  }
612
655
  });
613
656
  }
614
- if (core.Is.stringValue(envVars.azureCosmosdbKey)) {
657
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.AzureCosmosDb)) {
615
658
  coreConfig.types.entityStorageConnector.push({
616
659
  type: engineTypes.EntityStorageConnectorType.AzureCosmosDb,
617
660
  options: {
@@ -625,7 +668,7 @@ function configureEntityStorage(coreConfig, envVars) {
625
668
  }
626
669
  });
627
670
  }
628
- if (core.Is.stringValue(envVars.gcpFirestoreCredentials)) {
671
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.GcpFirestoreDb)) {
629
672
  coreConfig.types.entityStorageConnector.push({
630
673
  type: engineTypes.EntityStorageConnectorType.GcpFirestoreDb,
631
674
  options: {
@@ -640,41 +683,27 @@ function configureEntityStorage(coreConfig, envVars) {
640
683
  }
641
684
  });
642
685
  }
643
- if (core.Is.stringValue(envVars.scylladbHosts)) {
686
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.ScyllaDb)) {
644
687
  coreConfig.types.entityStorageConnector.push({
645
688
  type: engineTypes.EntityStorageConnectorType.ScyllaDb,
646
689
  options: {
647
690
  config: {
648
- hosts: envVars.scylladbHosts.split(",") ?? "",
691
+ hosts: envVars.scylladbHosts?.split(",") ?? [],
649
692
  localDataCenter: envVars.scylladbLocalDataCenter ?? "",
650
- keyspace: envVars.scylladbKeyspace ?? ""
651
- },
652
- tablePrefix: envVars.entityStorageTablePrefix
653
- }
654
- });
655
- }
656
- if (core.Is.stringValue(envVars.mySqlHost)) {
657
- coreConfig.types.entityStorageConnector.push({
658
- type: engineTypes.EntityStorageConnectorType.MySqlDb,
659
- options: {
660
- config: {
661
- host: envVars.mySqlHost,
662
- port: envVars.mySqlPort ?? 3306,
663
- user: envVars.mySqlUser ?? "",
664
- password: envVars.mySqlPassword ?? "",
665
- database: envVars.mySqlDatabase ?? ""
693
+ keyspace: envVars.scylladbKeyspace ?? "",
694
+ port: core.Coerce.integer(envVars.scylladbPort)
666
695
  },
667
696
  tablePrefix: envVars.entityStorageTablePrefix
668
697
  }
669
698
  });
670
699
  }
671
- if (core.Is.stringValue(envVars.mySqlHost)) {
700
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.MySqlDb)) {
672
701
  coreConfig.types.entityStorageConnector.push({
673
702
  type: engineTypes.EntityStorageConnectorType.MySqlDb,
674
703
  options: {
675
704
  config: {
676
- host: envVars.mySqlHost,
677
- port: envVars.mySqlPort ?? 3306,
705
+ host: envVars.mySqlHost ?? "",
706
+ port: core.Coerce.integer(envVars.mySqlPort),
678
707
  user: envVars.mySqlUser ?? "",
679
708
  password: envVars.mySqlPassword ?? "",
680
709
  database: envVars.mySqlDatabase ?? ""
@@ -683,13 +712,13 @@ function configureEntityStorage(coreConfig, envVars) {
683
712
  }
684
713
  });
685
714
  }
686
- if (core.Is.stringValue(envVars.mongoDbHost)) {
715
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.MongoDb)) {
687
716
  coreConfig.types.entityStorageConnector.push({
688
717
  type: engineTypes.EntityStorageConnectorType.MongoDb,
689
718
  options: {
690
719
  config: {
691
- host: envVars.mongoDbHost,
692
- port: envVars.mongoDbPort,
720
+ host: envVars.mongoDbHost ?? "",
721
+ port: core.Coerce.integer(envVars.mongoDbPort),
693
722
  user: envVars.mongoDbUser ?? "",
694
723
  password: envVars.mongoDbPassword ?? "",
695
724
  database: envVars.mongoDbDatabase ?? ""
@@ -698,13 +727,13 @@ function configureEntityStorage(coreConfig, envVars) {
698
727
  }
699
728
  });
700
729
  }
701
- if (core.Is.stringValue(envVars.postgreSqlHost)) {
730
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.PostgreSql)) {
702
731
  coreConfig.types.entityStorageConnector.push({
703
732
  type: engineTypes.EntityStorageConnectorType.PostgreSql,
704
733
  options: {
705
734
  config: {
706
- host: envVars.postgreSqlHost,
707
- port: envVars.postgreSqlPort,
735
+ host: envVars.postgreSqlHost ?? "",
736
+ port: core.Coerce.integer(envVars.postgreSqlPort),
708
737
  user: envVars.postgreSqlUser ?? "",
709
738
  password: envVars.postgreSqlPassword ?? "",
710
739
  database: envVars.postgreSqlDatabase ?? ""
@@ -713,11 +742,21 @@ function configureEntityStorage(coreConfig, envVars) {
713
742
  }
714
743
  });
715
744
  }
716
- const defaultStorageConnector = envVars.entityStorageConnectorType;
717
- if (core.Is.stringValue(defaultStorageConnector)) {
745
+ const defaultEntityStorageConnectorType = envVars.entityStorageConnectorDefault ?? entityStorageConnectorTypes[0];
746
+ if (entityStorageConnectorTypes.includes(engineTypes.EntityStorageConnectorType.Synchronised)) {
747
+ // For synchronised storage we use the default connector as the one we wrap for real DB operations
748
+ coreConfig.types.entityStorageConnector.push({
749
+ type: engineTypes.EntityStorageConnectorType.Synchronised,
750
+ options: {
751
+ entityStorageConnectorType: defaultEntityStorageConnectorType
752
+ }
753
+ });
754
+ }
755
+ if (core.Is.arrayValue(entityStorageConnectorTypes)) {
718
756
  for (const config of coreConfig.types.entityStorageConnector) {
719
- if (config.type === defaultStorageConnector) {
757
+ if (config.type === defaultEntityStorageConnectorType) {
720
758
  config.isDefault = true;
759
+ break;
721
760
  }
722
761
  }
723
762
  }
@@ -729,14 +768,13 @@ function configureEntityStorage(coreConfig, envVars) {
729
768
  */
730
769
  function configureBlobStorage(coreConfig, envVars) {
731
770
  coreConfig.types.blobStorageConnector ??= [];
732
- if ((core.Coerce.boolean(envVars.blobMemoryEnable) ?? false) ||
733
- envVars.blobStorageConnectorType === engineTypes.BlobStorageConnectorType.Memory) {
771
+ const blobStorageConnectorTypes = envVars.blobStorageConnectorType?.split(",") ?? [];
772
+ if (blobStorageConnectorTypes.includes(engineTypes.BlobStorageConnectorType.Memory)) {
734
773
  coreConfig.types.blobStorageConnector.push({
735
774
  type: engineTypes.BlobStorageConnectorType.Memory
736
775
  });
737
776
  }
738
- if ((core.Coerce.boolean(envVars.blobFileEnable) ?? false) ||
739
- envVars.blobStorageConnectorType === engineTypes.BlobStorageConnectorType.File) {
777
+ if (blobStorageConnectorTypes.includes(engineTypes.BlobStorageConnectorType.File)) {
740
778
  coreConfig.types.blobStorageConnector.push({
741
779
  type: engineTypes.BlobStorageConnectorType.File,
742
780
  options: {
@@ -749,18 +787,18 @@ function configureBlobStorage(coreConfig, envVars) {
749
787
  }
750
788
  });
751
789
  }
752
- if (core.Is.stringValue(envVars.ipfsApiUrl)) {
790
+ if (blobStorageConnectorTypes.includes(engineTypes.BlobStorageConnectorType.Ipfs)) {
753
791
  coreConfig.types.blobStorageConnector.push({
754
792
  type: engineTypes.BlobStorageConnectorType.Ipfs,
755
793
  options: {
756
794
  config: {
757
- apiUrl: envVars.ipfsApiUrl,
795
+ apiUrl: envVars.ipfsApiUrl ?? "",
758
796
  bearerToken: envVars.ipfsBearerToken
759
797
  }
760
798
  }
761
799
  });
762
800
  }
763
- if (core.Is.stringValue(envVars.awsS3AccessKeyId)) {
801
+ if (blobStorageConnectorTypes.includes(engineTypes.BlobStorageConnectorType.AwsS3)) {
764
802
  coreConfig.types.blobStorageConnector.push({
765
803
  type: engineTypes.BlobStorageConnectorType.AwsS3,
766
804
  options: {
@@ -775,7 +813,7 @@ function configureBlobStorage(coreConfig, envVars) {
775
813
  }
776
814
  });
777
815
  }
778
- if (core.Is.stringValue(envVars.azureStorageAccountKey)) {
816
+ if (blobStorageConnectorTypes.includes(engineTypes.BlobStorageConnectorType.AzureStorage)) {
779
817
  coreConfig.types.blobStorageConnector.push({
780
818
  type: engineTypes.BlobStorageConnectorType.AzureStorage,
781
819
  options: {
@@ -789,7 +827,7 @@ function configureBlobStorage(coreConfig, envVars) {
789
827
  }
790
828
  });
791
829
  }
792
- if (core.Is.stringValue(envVars.gcpStorageCredentials)) {
830
+ if (blobStorageConnectorTypes.includes(engineTypes.BlobStorageConnectorType.GcpStorage)) {
793
831
  coreConfig.types.blobStorageConnector.push({
794
832
  type: engineTypes.BlobStorageConnectorType.GcpStorage,
795
833
  options: {
@@ -803,12 +841,20 @@ function configureBlobStorage(coreConfig, envVars) {
803
841
  }
804
842
  });
805
843
  }
806
- const defaultStorageConnectorType = envVars.blobStorageConnectorType;
807
- if (core.Is.stringValue(defaultStorageConnectorType)) {
844
+ if (core.Is.arrayValue(blobStorageConnectorTypes)) {
845
+ const defaultStorageConnectorType = envVars.blobStorageConnectorDefault ?? blobStorageConnectorTypes[0];
808
846
  for (const config of coreConfig.types.blobStorageConnector) {
809
847
  if (config.type === defaultStorageConnectorType) {
810
848
  config.isDefault = true;
811
849
  }
850
+ // If this blob storage connector is the one to use for public access
851
+ // then add it as a feature
852
+ if (core.Is.stringValue(envVars.blobStorageConnectorPublic) &&
853
+ config.type === envVars.blobStorageConnectorPublic) {
854
+ config.features ??= [];
855
+ config.features.push("public");
856
+ break;
857
+ }
812
858
  }
813
859
  }
814
860
  if (coreConfig.types.blobStorageConnector.length > 0) {
@@ -818,7 +864,7 @@ function configureBlobStorage(coreConfig, envVars) {
818
864
  options: {
819
865
  config: {
820
866
  vaultKeyId: (envVars.blobStorageEnableEncryption ?? false)
821
- ? envVars.blobStorageEncryptionKey
867
+ ? envVars.blobStorageEncryptionKeyId
822
868
  : undefined
823
869
  }
824
870
  }
@@ -1313,51 +1359,168 @@ function configureDocumentManagement(coreConfig, envVars) {
1313
1359
  }
1314
1360
  }
1315
1361
  /**
1316
- * Configures the federated catalogue.
1362
+ * Configures the rights management.
1317
1363
  * @param coreConfig The core config.
1318
1364
  * @param envVars The environment variables.
1319
1365
  */
1320
- function configureFederatedCatalogue(coreConfig, envVars) {
1321
- if (core.Is.arrayValue(coreConfig.types.identityResolverComponent)) {
1322
- coreConfig.types.federatedCatalogueComponent ??= [];
1323
- coreConfig.types.federatedCatalogueComponent.push({
1324
- type: engineTypes.FederatedCatalogueComponentType.Service,
1366
+ function configureRightsManagement(coreConfig, envVars) {
1367
+ if (core.Coerce.boolean(envVars.rightsManagementEnabled) ?? false) {
1368
+ coreConfig.types.rightsManagementPapComponent ??= [];
1369
+ coreConfig.types.rightsManagementPapComponent.push({
1370
+ type: engineTypes.RightsManagementPapComponentType.Service
1371
+ });
1372
+ coreConfig.types.rightsManagementPmpComponent ??= [];
1373
+ coreConfig.types.rightsManagementPmpComponent.push({
1374
+ type: engineTypes.RightsManagementPmpComponentType.Service
1375
+ });
1376
+ coreConfig.types.rightsManagementPipComponent ??= [];
1377
+ coreConfig.types.rightsManagementPipComponent.push({
1378
+ type: engineTypes.RightsManagementPipComponentType.Service,
1379
+ options: {
1380
+ informationModulesConfig: core.Is.arrayValue(envVars.rightsManagementInformationSources)
1381
+ ? envVars.rightsManagementInformationSources
1382
+ : undefined
1383
+ }
1384
+ });
1385
+ coreConfig.types.rightsManagementPxpComponent ??= [];
1386
+ coreConfig.types.rightsManagementPxpComponent.push({
1387
+ type: engineTypes.RightsManagementPxpComponentType.Service,
1388
+ options: {
1389
+ actionModulesConfig: core.Is.arrayValue(envVars.rightsManagementExecutionActions)
1390
+ ? envVars.rightsManagementExecutionActions
1391
+ : undefined
1392
+ }
1393
+ });
1394
+ coreConfig.types.rightsManagementPdpComponent ??= [];
1395
+ coreConfig.types.rightsManagementPdpComponent.push({
1396
+ type: engineTypes.RightsManagementPdpComponentType.Service
1397
+ });
1398
+ coreConfig.types.rightsManagementPepComponent ??= [];
1399
+ coreConfig.types.rightsManagementPepComponent.push({
1400
+ type: engineTypes.RightsManagementPepComponentType.Service,
1401
+ options: {
1402
+ processorModulesConfig: core.Is.arrayValue(envVars.rightsManagementEnforcementProcessors)
1403
+ ? envVars.rightsManagementEnforcementProcessors
1404
+ : undefined
1405
+ }
1406
+ });
1407
+ coreConfig.types.rightsManagementPnpComponent ??= [];
1408
+ coreConfig.types.rightsManagementPnpComponent.push({
1409
+ type: engineTypes.RightsManagementPnpComponentType.Service,
1410
+ options: {
1411
+ negotiatorModulesConfig: core.Is.arrayValue(envVars.rightsManagementNegotiators)
1412
+ ? envVars.rightsManagementNegotiators
1413
+ : undefined
1414
+ }
1415
+ });
1416
+ coreConfig.types.rightsManagementPnapComponent ??= [];
1417
+ coreConfig.types.rightsManagementPnapComponent.push({
1418
+ type: engineTypes.RightsManagementPnapComponentType.Service
1419
+ });
1420
+ coreConfig.types.rightsManagementPnrpComponent ??= [];
1421
+ coreConfig.types.rightsManagementPnrpComponent.push({
1422
+ type: engineTypes.RightsManagementPnrpComponentType.Service,
1325
1423
  options: {
1326
1424
  config: {
1327
- subResourceCacheTtlMs: core.Coerce.number(envVars.federatedCatalogueCacheTtlMs),
1328
- clearingHouseApproverList: core.Coerce.object(envVars.federatedCatalogueClearingHouseApproverList) ?? []
1425
+ negotiationMethodId: envVars.rightsManagementNegotiationMethodId ?? "",
1426
+ negotiationComponentCreator: async (url) => new rightsManagementRestClient.PolicyNegotiationPointClient({ endpoint: url })
1329
1427
  }
1330
1428
  }
1331
1429
  });
1332
1430
  }
1333
1431
  }
1334
1432
  /**
1335
- * Configures the rights management.
1433
+ * Configures the task scheduler.
1336
1434
  * @param coreConfig The core config.
1337
1435
  * @param envVars The environment variables.
1338
1436
  */
1339
- function configureRightsManagement(coreConfig, envVars) {
1340
- if (core.Coerce.boolean(envVars.rightsManagementEnabled) ?? false) {
1341
- coreConfig.types.rightsManagementPapComponent ??= [];
1342
- coreConfig.types.rightsManagementPapComponent.push({
1343
- type: engineTypes.RightsManagementPapComponentType.Service
1437
+ function configureTaskScheduler(coreConfig, envVars) {
1438
+ if (core.Coerce.boolean(envVars.taskSchedulerEnabled) ?? false) {
1439
+ coreConfig.types.taskSchedulerComponent ??= [];
1440
+ coreConfig.types.taskSchedulerComponent.push({
1441
+ type: engineTypes.TaskSchedulerComponentType.Service
1344
1442
  });
1345
- coreConfig.types.rightsManagementComponent ??= [];
1346
- coreConfig.types.rightsManagementComponent.push({
1347
- type: engineTypes.RightsManagementComponentType.Service
1443
+ }
1444
+ }
1445
+ /**
1446
+ * Configures the synchronised storage.
1447
+ * @param coreConfig The core config.
1448
+ * @param envVars The environment variables.
1449
+ */
1450
+ function configureSynchronisedStorage(coreConfig, envVars) {
1451
+ if (core.Is.arrayValue(coreConfig.types.identityResolverComponent) &&
1452
+ (core.Coerce.boolean(envVars.synchronisedStorageEnabled) ?? false)) {
1453
+ // Check if the config provides a custom verifiable storage key id
1454
+ let verifiableStorageKeyId = core.Coerce.string(envVars.synchronisedStorageVerifiableStorageKeyId);
1455
+ if (!core.Is.stringValue(verifiableStorageKeyId)) {
1456
+ // No custom key so default to the network setting
1457
+ verifiableStorageKeyId = envVars.iotaNetwork;
1458
+ }
1459
+ coreConfig.types.synchronisedStorageComponent ??= [];
1460
+ coreConfig.types.synchronisedStorageComponent.push({
1461
+ type: engineTypes.SynchronisedStorageComponentType.Service,
1462
+ options: {
1463
+ config: {
1464
+ verifiableStorageKeyId: verifiableStorageKeyId ?? "",
1465
+ synchronisedStorageMethodId: envVars.synchronisedStorageVerificationMethodId,
1466
+ blobStorageEncryptionKeyId: envVars.synchronisedStorageBlobStorageEncryptionKeyId,
1467
+ entityUpdateIntervalMinutes: core.Coerce.number(envVars.synchronisedStorageEntityUpdateIntervalMinutes),
1468
+ consolidationIntervalMinutes: core.Coerce.number(envVars.synchronisedStorageConsolidationIntervalMinutes),
1469
+ consolidationBatchSize: core.Coerce.number(envVars.synchronisedStorageConsolidationBatchSize),
1470
+ maxConsolidations: core.Coerce.number(envVars.synchronisedStorageMaxConsolidations)
1471
+ }
1472
+ }
1348
1473
  });
1474
+ // If there is a trusted url set, we need to add a client
1475
+ // and give it a feature of trusted so that when the synchronised
1476
+ // storage is created it can pickup the correct component
1477
+ if (core.Is.stringValue(envVars.synchronisedStorageTrustedUrl)) {
1478
+ coreConfig.types.synchronisedStorageComponent.push({
1479
+ type: engineTypes.SynchronisedStorageComponentType.RestClient,
1480
+ options: {
1481
+ endpoint: envVars.synchronisedStorageTrustedUrl
1482
+ },
1483
+ features: ["trusted"]
1484
+ });
1485
+ }
1349
1486
  }
1350
1487
  }
1351
1488
  /**
1352
- * Configures the task scheduler.
1489
+ * Configures the federated catalogue.
1353
1490
  * @param coreConfig The core config.
1354
1491
  * @param envVars The environment variables.
1355
1492
  */
1356
- function configureTaskScheduler(coreConfig, envVars) {
1357
- if (core.Coerce.boolean(envVars.taskSchedulerEnabled) ?? true) {
1358
- coreConfig.types.taskSchedulerComponent ??= [];
1359
- coreConfig.types.taskSchedulerComponent.push({
1360
- type: engineTypes.TaskSchedulerComponentType.Default
1493
+ function configureFederatedCatalogue(coreConfig, envVars) {
1494
+ if (core.Coerce.boolean(envVars.federatedCatalogueEnabled) ?? false) {
1495
+ coreConfig.types.federatedCatalogueComponent ??= [];
1496
+ coreConfig.types.federatedCatalogueComponent.push({
1497
+ type: engineTypes.FederatedCatalogueComponentType.Service,
1498
+ options: {
1499
+ config: {
1500
+ subResourceCacheTtlMs: core.Coerce.number(envVars.federatedCatalogueCacheTtlMs),
1501
+ clearingHouseApproverList: core.Coerce.object(envVars.federatedCatalogueClearingHouseApproverList) ?? []
1502
+ }
1503
+ }
1504
+ });
1505
+ }
1506
+ }
1507
+ /**
1508
+ * Configures the data space connector.
1509
+ * @param coreConfig The core config.
1510
+ * @param envVars The environment variables.
1511
+ */
1512
+ function configureDataSpaceConnector(coreConfig, envVars) {
1513
+ if (core.Coerce.boolean(envVars.dataSpaceConnectorEnabled) ?? false) {
1514
+ coreConfig.types.dataSpaceConnectorComponent ??= [];
1515
+ coreConfig.types.dataSpaceConnectorComponent.push({
1516
+ type: engineTypes.DataSpaceConnectorComponentType.Service,
1517
+ options: {
1518
+ config: {
1519
+ dataSpaceConnectorAppDescriptors: core.Is.arrayValue(envVars.dataSpaceConnectorApps)
1520
+ ? envVars.dataSpaceConnectorApps
1521
+ : undefined
1522
+ }
1523
+ }
1361
1524
  });
1362
1525
  }
1363
1526
  }
@@ -1400,9 +1563,10 @@ function configureDlt(coreConfig, envVars) {
1400
1563
  * @param coreEngineConfig The core engine config.
1401
1564
  * @param serverInfo The server information.
1402
1565
  * @param openApiSpecPath The path to the open api spec.
1566
+ * @param favIconPath The path to the favicon.
1403
1567
  * @returns The the config for the core and the server.
1404
1568
  */
1405
- function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, openApiSpecPath) {
1569
+ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, openApiSpecPath, favIconPath) {
1406
1570
  envVars.authSigningKeyId ??= "auth-signing";
1407
1571
  const webServerOptions = {
1408
1572
  port: core.Coerce.number(envVars.port),
@@ -1429,7 +1593,8 @@ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, o
1429
1593
  options: {
1430
1594
  config: {
1431
1595
  serverInfo,
1432
- openApiSpecPath
1596
+ openApiSpecPath,
1597
+ favIconPath
1433
1598
  }
1434
1599
  }
1435
1600
  }
@@ -1546,16 +1711,19 @@ function buildEngineServerConfiguration(envVars, coreEngineConfig, serverInfo, o
1546
1711
  * @returns The engine server.
1547
1712
  */
1548
1713
  async function start(nodeOptions, engineServerConfig, envVars) {
1549
- envVars.storageFileRoot ??= "";
1550
- if ((envVars.entityStorageConnectorType === engineTypes.EntityStorageConnectorType.File ||
1551
- envVars.blobStorageConnectorType === engineTypes.BlobStorageConnectorType.File ||
1714
+ const entityStorageConnectorType = envVars.entityStorageConnectorType?.split(",") ?? [];
1715
+ const blobStorageConnectorType = envVars.blobStorageConnectorType?.split(",") ?? [];
1716
+ // If the blob storage or entity storage is configured with file connectors
1717
+ // then we need to make sure the storageFileRoot is set
1718
+ if ((entityStorageConnectorType.includes(engineTypes.EntityStorageConnectorType.File) ||
1719
+ blobStorageConnectorType.includes(engineTypes.BlobStorageConnectorType.File) ||
1552
1720
  core.Is.empty(nodeOptions?.stateStorage)) &&
1553
1721
  !core.Is.stringValue(envVars.storageFileRoot)) {
1554
1722
  throw new core.GeneralError("node", "storageFileRootNotSet", {
1555
1723
  storageFileRoot: `${nodeOptions?.envPrefix ?? ""}_STORAGE_FILE_ROOT`
1556
1724
  });
1557
1725
  }
1558
- // Create the engine instance using file state storage and custom bootstrap.
1726
+ // Create the engine instance using file state storage unless one is configured in options
1559
1727
  const engine$1 = new engine.Engine({
1560
1728
  config: engineServerConfig,
1561
1729
  stateStorage: nodeOptions?.stateStorage ?? new engineCore.FileStateStorage(envVars.stateFilename ?? ""),
@@ -1599,7 +1767,7 @@ async function run(nodeOptions) {
1599
1767
  nodeOptions ??= {};
1600
1768
  const serverInfo = {
1601
1769
  name: nodeOptions?.serverName ?? "TWIN Node Server",
1602
- version: nodeOptions?.serverVersion ?? "0.0.2-next.1" // x-release-please-version
1770
+ version: nodeOptions?.serverVersion ?? "0.0.2-next.10" // x-release-please-version
1603
1771
  };
1604
1772
  console.log(`\u001B[4mđŸŒŠī¸ ${serverInfo.name} v${serverInfo.version}\u001B[24m\n`);
1605
1773
  if (!core.Is.stringValue(nodeOptions?.executionDirectory)) {
@@ -1612,15 +1780,24 @@ async function run(nodeOptions) {
1612
1780
  console.info("Locales Directory:", nodeOptions.localesDirectory);
1613
1781
  await initialiseLocales(nodeOptions.localesDirectory);
1614
1782
  if (core.Is.empty(nodeOptions?.openApiSpecFile)) {
1615
- const specFile = path.resolve(path.join(nodeOptions.executionDirectory, "docs", "open-api", "spec.json"));
1783
+ const specFile = path.resolve(path.join(nodeOptions.executionDirectory ?? "", "docs", "open-api", "spec.json"));
1616
1784
  console.info("Default OpenAPI Spec File:", specFile);
1617
1785
  if (await fileExists(specFile)) {
1618
1786
  nodeOptions ??= {};
1619
1787
  nodeOptions.openApiSpecFile = specFile;
1620
1788
  }
1621
1789
  }
1790
+ if (core.Is.empty(nodeOptions?.favIconFile)) {
1791
+ const favIconFile = path.resolve(path.join(nodeOptions.executionDirectory ?? "", "static", "favicon.png"));
1792
+ console.info("Default Favicon File:", favIconFile);
1793
+ if (await fileExists(favIconFile)) {
1794
+ nodeOptions ??= {};
1795
+ nodeOptions.favIconFile = favIconFile;
1796
+ }
1797
+ }
1622
1798
  nodeOptions.envPrefix ??= "TWIN_NODE_";
1623
1799
  console.info("Environment Prefix:", nodeOptions.envPrefix);
1800
+ overrideModuleImport(nodeOptions.executionDirectory ?? "");
1624
1801
  const { engineServerConfig, nodeEnvVars: envVars } = await buildConfiguration(process.env, nodeOptions, serverInfo);
1625
1802
  console.info();
1626
1803
  const startResult = await start(nodeOptions, engineServerConfig, envVars);
@@ -1657,7 +1834,8 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1657
1834
  }
1658
1835
  if (core.Is.arrayValue(options?.envFilenames)) {
1659
1836
  const output = dotenv__namespace.config({
1660
- path: options?.envFilenames
1837
+ path: options?.envFilenames,
1838
+ quiet: true
1661
1839
  });
1662
1840
  // We don't want to throw an error if the default environment file is not found.
1663
1841
  // Only if we have custom environment files.
@@ -1672,12 +1850,18 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1672
1850
  // Expand any environment variables that use the @file: syntax
1673
1851
  const keys = Object.keys(envVars);
1674
1852
  for (const key of keys) {
1675
- if (core.Is.stringValue(envVars[key]) && envVars[key].startsWith("@file:")) {
1853
+ if (core.Is.stringValue(envVars[key]) &&
1854
+ (envVars[key].startsWith("@text:") || envVars[key].startsWith("@json:"))) {
1676
1855
  const filePath = envVars[key].slice(6).trim();
1677
1856
  const embeddedFile = path.resolve(path.join(options.executionDirectory ?? "", filePath));
1678
- console.info(`Expanding Environment Variable: ${key} from file: ${embeddedFile}`);
1679
- const fileContent = await loadJsonFile(embeddedFile);
1680
- envVars[key] = fileContent;
1857
+ if (envVars[key].startsWith("@text:")) {
1858
+ console.info(`Expanding Environment Variable: ${key} from text file: ${embeddedFile}`);
1859
+ envVars[key] = await loadTextFile(embeddedFile);
1860
+ }
1861
+ else if (envVars[key].startsWith("@json:")) {
1862
+ console.info(`Expanding Environment Variable: ${key} from JSON file: ${embeddedFile}`);
1863
+ envVars[key] = await loadJsonFile(embeddedFile);
1864
+ }
1681
1865
  }
1682
1866
  }
1683
1867
  // Extend the environment variables with any additional custom configuration.
@@ -1687,7 +1871,7 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1687
1871
  }
1688
1872
  // Build the engine configuration from the environment variables.
1689
1873
  const coreConfig = buildEngineConfiguration(envVars);
1690
- const engineServerConfig = buildEngineServerConfiguration(envVars, coreConfig, serverInfo, options?.openApiSpecFile);
1874
+ const engineServerConfig = buildEngineServerConfiguration(envVars, coreConfig, serverInfo, options?.openApiSpecFile, options?.favIconFile);
1691
1875
  // Merge any custom configuration provided in the options.
1692
1876
  if (core.Is.arrayValue(options?.configFilenames)) {
1693
1877
  for (const configFile of options.configFilenames) {
@@ -1708,15 +1892,49 @@ async function buildConfiguration(processEnv, options, serverInfo) {
1708
1892
  }
1709
1893
  return { engineServerConfig, nodeEnvVars: envVars };
1710
1894
  }
1895
+ /**
1896
+ * Override module imports to use local files where possible.
1897
+ * @param executionDirectory The execution directory for resolving local module paths.
1898
+ */
1899
+ function overrideModuleImport(executionDirectory) {
1900
+ modules.ModuleHelper.overrideImport(async (moduleName) => {
1901
+ // If the module path for example when dynamically loading
1902
+ // modules looks like a local file then we try to resolve
1903
+ // using the local file system
1904
+ const isLocal = modules.ModuleHelper.isLocalModule(moduleName);
1905
+ if (isLocal) {
1906
+ // See if we can just resolve the filename locally
1907
+ let localFilename = path.resolve(moduleName);
1908
+ let exists = await fileExists(localFilename);
1909
+ if (!exists) {
1910
+ // Doesn't exist in the current directory, try the execution directory
1911
+ localFilename = path.resolve(executionDirectory, moduleName);
1912
+ exists = await fileExists(localFilename);
1913
+ }
1914
+ if (exists) {
1915
+ // If the module exists then we can load it, otherwise
1916
+ // we fallback to regular handling to see if that can import it
1917
+ return {
1918
+ module: await import(process.platform === "win32" ? `file://${localFilename}` : localFilename),
1919
+ useDefault: false
1920
+ };
1921
+ }
1922
+ }
1923
+ // The filename doesn't look like a local module, so just use default handling
1924
+ return {
1925
+ useDefault: true
1926
+ };
1927
+ });
1928
+ }
1711
1929
 
1712
1930
  exports.NodeFeatures = NodeFeatures;
1713
1931
  exports.bootstrap = bootstrap;
1714
- exports.bootstrapAttestationMethod = bootstrapAttestationMethod;
1715
1932
  exports.bootstrapAuth = bootstrapAuth;
1716
1933
  exports.bootstrapBlobEncryption = bootstrapBlobEncryption;
1717
1934
  exports.bootstrapImmutableProofMethod = bootstrapImmutableProofMethod;
1718
1935
  exports.bootstrapNodeIdentity = bootstrapNodeIdentity;
1719
1936
  exports.bootstrapNodeUser = bootstrapNodeUser;
1937
+ exports.bootstrapSynchronisedStorage = bootstrapSynchronisedStorage;
1720
1938
  exports.buildConfiguration = buildConfiguration;
1721
1939
  exports.buildEngineConfiguration = buildEngineConfiguration;
1722
1940
  exports.buildEngineServerConfiguration = buildEngineServerConfiguration;
@@ -1725,5 +1943,7 @@ exports.getExecutionDirectory = getExecutionDirectory;
1725
1943
  exports.getFeatures = getFeatures;
1726
1944
  exports.initialiseLocales = initialiseLocales;
1727
1945
  exports.loadJsonFile = loadJsonFile;
1946
+ exports.loadTextFile = loadTextFile;
1947
+ exports.overrideModuleImport = overrideModuleImport;
1728
1948
  exports.run = run;
1729
1949
  exports.start = start;