@twin.org/node-core 0.0.1-next.2
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.
- package/LICENSE +201 -0
- package/README.md +33 -0
- package/dist/cjs/index.cjs +612 -0
- package/dist/esm/index.mjs +578 -0
- package/dist/types/bootstrap.d.ts +60 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/models/INodeState.d.ts +10 -0
- package/dist/types/models/INodeVariables.d.ts +29 -0
- package/dist/types/models/nodeFeatures.d.ts +17 -0
- package/dist/types/node.d.ts +21 -0
- package/dist/types/server.d.ts +21 -0
- package/dist/types/utils.d.ts +24 -0
- package/docs/changelog.md +8 -0
- package/docs/reference/functions/bootstrap.md +29 -0
- package/docs/reference/functions/bootstrapAttestationMethod.md +35 -0
- package/docs/reference/functions/bootstrapAuth.md +35 -0
- package/docs/reference/functions/bootstrapBlobEncryption.md +35 -0
- package/docs/reference/functions/bootstrapImmutableProofMethod.md +35 -0
- package/docs/reference/functions/bootstrapNodeIdentity.md +35 -0
- package/docs/reference/functions/bootstrapNodeUser.md +35 -0
- package/docs/reference/functions/fileExists.md +19 -0
- package/docs/reference/functions/getExecutionDirectory.md +11 -0
- package/docs/reference/functions/getFeatures.md +19 -0
- package/docs/reference/functions/initialiseLocales.md +17 -0
- package/docs/reference/functions/run.md +59 -0
- package/docs/reference/functions/start.md +43 -0
- package/docs/reference/index.md +30 -0
- package/docs/reference/interfaces/INodeState.md +15 -0
- package/docs/reference/interfaces/INodeVariables.md +59 -0
- package/docs/reference/type-aliases/NodeFeatures.md +5 -0
- package/docs/reference/variables/NodeFeatures.md +19 -0
- package/locales/en.json +34 -0
- package/package.json +52 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var apiAuthEntityStorageService = require('@twin.org/api-auth-entity-storage-service');
|
|
4
|
+
var core = require('@twin.org/core');
|
|
5
|
+
var crypto = require('@twin.org/crypto');
|
|
6
|
+
var engineTypes = require('@twin.org/engine-types');
|
|
7
|
+
var entityStorageModels = require('@twin.org/entity-storage-models');
|
|
8
|
+
var identityModels = require('@twin.org/identity-models');
|
|
9
|
+
var vaultModels = require('@twin.org/vault-models');
|
|
10
|
+
var walletModels = require('@twin.org/wallet-models');
|
|
11
|
+
var promises = require('node:fs/promises');
|
|
12
|
+
var path = require('node:path');
|
|
13
|
+
var dotenv = require('dotenv');
|
|
14
|
+
var engine = require('@twin.org/engine');
|
|
15
|
+
var engineCore = require('@twin.org/engine-core');
|
|
16
|
+
var engineModels = require('@twin.org/engine-models');
|
|
17
|
+
var engineServer = require('@twin.org/engine-server');
|
|
18
|
+
|
|
19
|
+
function _interopNamespaceDefault(e) {
|
|
20
|
+
var n = Object.create(null);
|
|
21
|
+
if (e) {
|
|
22
|
+
Object.keys(e).forEach(function (k) {
|
|
23
|
+
if (k !== 'default') {
|
|
24
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
25
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
get: function () { return e[k]; }
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
n.default = e;
|
|
33
|
+
return Object.freeze(n);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var dotenv__namespace = /*#__PURE__*/_interopNamespaceDefault(dotenv);
|
|
37
|
+
|
|
38
|
+
// Copyright 2024 IOTA Stiftung.
|
|
39
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
40
|
+
/**
|
|
41
|
+
* The features that can be enabled on the node.
|
|
42
|
+
*/
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
44
|
+
const NodeFeatures = {
|
|
45
|
+
/**
|
|
46
|
+
* NodeIdentity - generates an identity for the node if not provided in config.
|
|
47
|
+
*/
|
|
48
|
+
NodeIdentity: "node-identity",
|
|
49
|
+
/**
|
|
50
|
+
* NodeUser - generates a user for the node if not provided in config.
|
|
51
|
+
*/
|
|
52
|
+
NodeUser: "node-user"
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Copyright 2024 IOTA Stiftung.
|
|
56
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
57
|
+
/* eslint-disable no-console */
|
|
58
|
+
/**
|
|
59
|
+
* Initialise the locales for the application.
|
|
60
|
+
* @param localesDirectory The directory containing the locales.
|
|
61
|
+
*/
|
|
62
|
+
async function initialiseLocales(localesDirectory) {
|
|
63
|
+
const localesFile = path.resolve(path.join(localesDirectory, "en.json"));
|
|
64
|
+
console.info("Locales File:", localesFile);
|
|
65
|
+
if (await fileExists(localesFile)) {
|
|
66
|
+
const enLangContent = await promises.readFile(localesFile, "utf8");
|
|
67
|
+
core.I18n.addDictionary("en", JSON.parse(enLangContent));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.warn(`Locales file not found: ${localesFile}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get the directory where the application is being executed.
|
|
75
|
+
* @returns The execution directory.
|
|
76
|
+
*/
|
|
77
|
+
function getExecutionDirectory() {
|
|
78
|
+
return process.cwd();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Does the specified file exist.
|
|
82
|
+
* @param filename The filename to check for existence.
|
|
83
|
+
* @returns True if the file exists.
|
|
84
|
+
*/
|
|
85
|
+
async function fileExists(filename) {
|
|
86
|
+
try {
|
|
87
|
+
const stats = await promises.stat(filename);
|
|
88
|
+
return stats.isFile();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get the features that are enabled on the node.
|
|
96
|
+
* @param env The environment variables for the node.
|
|
97
|
+
* @returns The features that are enabled on the node.
|
|
98
|
+
*/
|
|
99
|
+
function getFeatures(env) {
|
|
100
|
+
if (core.Is.empty(env.features)) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const features = [];
|
|
104
|
+
const allFeatures = Object.values(NodeFeatures);
|
|
105
|
+
const splitFeatures = env.features.split(",");
|
|
106
|
+
for (const feature of splitFeatures) {
|
|
107
|
+
const featureTrimmed = feature.trim();
|
|
108
|
+
if (allFeatures.includes(featureTrimmed)) {
|
|
109
|
+
features.push(featureTrimmed);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return features;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Copyright 2024 IOTA Stiftung.
|
|
116
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
117
|
+
const DEFAULT_NODE_USERNAME = "admin@node";
|
|
118
|
+
/**
|
|
119
|
+
* Bootstrap the application.
|
|
120
|
+
* @param engineCore The engine core for the node.
|
|
121
|
+
* @param context The context for the node.
|
|
122
|
+
* @param envVars The environment variables for the node.
|
|
123
|
+
*/
|
|
124
|
+
async function bootstrap(engineCore, context, envVars) {
|
|
125
|
+
const features = getFeatures(envVars);
|
|
126
|
+
await bootstrapNodeIdentity(engineCore, context, envVars, features);
|
|
127
|
+
await bootstrapNodeUser(engineCore, context, envVars, features);
|
|
128
|
+
await bootstrapAuth(engineCore, context, envVars);
|
|
129
|
+
await bootstrapBlobEncryption(engineCore, context, envVars);
|
|
130
|
+
await bootstrapAttestationMethod(engineCore, context, envVars);
|
|
131
|
+
await bootstrapImmutableProofMethod(engineCore, context, envVars);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Bootstrap the node creating any necessary resources.
|
|
135
|
+
* @param engineCore The engine core for the node.
|
|
136
|
+
* @param context The context for the node.
|
|
137
|
+
* @param envVars The environment variables for the node.
|
|
138
|
+
* @param features The features that are enabled on the node. The features that are enabled on the node.
|
|
139
|
+
*/
|
|
140
|
+
async function bootstrapNodeIdentity(engineCore, context, envVars, features) {
|
|
141
|
+
if (features.includes(NodeFeatures.NodeIdentity)) {
|
|
142
|
+
// When we bootstrap the node we need to generate an identity for it,
|
|
143
|
+
// But we have a chicken and egg problem in that we can't create the identity
|
|
144
|
+
// to store the mnemonic in the vault without an identity. We use a temporary identity
|
|
145
|
+
// and then replace it with the new identity later in the process.
|
|
146
|
+
const engineDefaultTypes = engineCore.getDefaultTypes();
|
|
147
|
+
if (!core.Is.empty(engineDefaultTypes.vaultConnector)) {
|
|
148
|
+
const vaultConnector = vaultModels.VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
|
|
149
|
+
const workingIdentity = envVars.identity ??
|
|
150
|
+
context.state.nodeIdentity ??
|
|
151
|
+
`bootstrap-temp-${core.Converter.bytesToHex(core.RandomHelper.generate(16))}`;
|
|
152
|
+
await bootstrapMnemonic(engineCore, envVars, features, vaultConnector, workingIdentity);
|
|
153
|
+
const addresses = await bootstrapWallet(engineCore, envVars, features, workingIdentity);
|
|
154
|
+
const finalIdentity = await bootstrapIdentity(engineCore, envVars, features, workingIdentity);
|
|
155
|
+
await finaliseWallet(engineCore, envVars, features, finalIdentity, addresses);
|
|
156
|
+
await finaliseMnemonic(vaultConnector, workingIdentity, finalIdentity);
|
|
157
|
+
context.state.nodeIdentity = finalIdentity;
|
|
158
|
+
context.state.addresses = addresses;
|
|
159
|
+
context.stateDirty = true;
|
|
160
|
+
engineCore.logInfo(core.I18n.formatMessage("node.nodeIdentity", {
|
|
161
|
+
identity: context.state.nodeIdentity
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Bootstrap the identity for the node.
|
|
168
|
+
* @param engineCore The engine core for the node.
|
|
169
|
+
* @param envVars The environment variables for the node.
|
|
170
|
+
* @param features The features that are enabled on the node. The features that are enabled on the node.
|
|
171
|
+
* @param nodeIdentity The identity of the node.
|
|
172
|
+
* @returns The addresses for the wallet.
|
|
173
|
+
*/
|
|
174
|
+
async function bootstrapIdentity(engineCore, envVars, features, nodeIdentity) {
|
|
175
|
+
const engineDefaultTypes = engineCore.getDefaultTypes();
|
|
176
|
+
// Now create an identity for the node controlled by the address we just funded
|
|
177
|
+
const identityConnector = identityModels.IdentityConnectorFactory.get(engineDefaultTypes.identityConnector);
|
|
178
|
+
let identityDocument;
|
|
179
|
+
try {
|
|
180
|
+
const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(engineDefaultTypes.identityResolverConnector);
|
|
181
|
+
identityDocument = await identityResolverConnector.resolveDocument(nodeIdentity);
|
|
182
|
+
engineCore.logInfo(core.I18n.formatMessage("node.existingNodeIdentity"));
|
|
183
|
+
}
|
|
184
|
+
catch { }
|
|
185
|
+
if (core.Is.empty(identityDocument)) {
|
|
186
|
+
engineCore.logInfo(core.I18n.formatMessage("node.generatingNodeIdentity"));
|
|
187
|
+
identityDocument = await identityConnector.createDocument(nodeIdentity);
|
|
188
|
+
}
|
|
189
|
+
if (engineDefaultTypes.identityConnector === engineTypes.IdentityConnectorType.Iota) {
|
|
190
|
+
const didUrn = core.Urn.fromValidString(identityDocument.id);
|
|
191
|
+
const didParts = didUrn.parts();
|
|
192
|
+
const objectId = didParts[3];
|
|
193
|
+
engineCore.logInfo(core.I18n.formatMessage("node.identityExplorer", {
|
|
194
|
+
url: `${envVars.iotaExplorerEndpoint}object/${objectId}?network=${envVars.iotaNetwork}`
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
return identityDocument.id;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Bootstrap the wallet for the node.
|
|
201
|
+
* @param engineCore The engine core for the node.
|
|
202
|
+
* @param envVars The environment variables for the node.
|
|
203
|
+
* @param features The features that are enabled on the node.
|
|
204
|
+
* @param nodeIdentity The identity of the node.
|
|
205
|
+
* @returns The addresses for the wallet.
|
|
206
|
+
*/
|
|
207
|
+
async function bootstrapWallet(engineCore, envVars, features, nodeIdentity) {
|
|
208
|
+
const engineDefaultTypes = engineCore.getDefaultTypes();
|
|
209
|
+
const walletConnector = walletModels.WalletConnectorFactory.get(engineDefaultTypes.walletConnector);
|
|
210
|
+
const addresses = await walletConnector.getAddresses(nodeIdentity, 0, 0, 5);
|
|
211
|
+
const balance = await walletConnector.getBalance(nodeIdentity, addresses[0]);
|
|
212
|
+
if (balance === 0n) {
|
|
213
|
+
let address0 = addresses[0];
|
|
214
|
+
if (engineDefaultTypes.walletConnector === engineTypes.WalletConnectorType.Iota) {
|
|
215
|
+
address0 = `${envVars.iotaExplorerEndpoint}address/${address0}?network=${envVars.iotaNetwork}`;
|
|
216
|
+
}
|
|
217
|
+
engineCore.logInfo(core.I18n.formatMessage("node.fundingWallet", { address: address0 }));
|
|
218
|
+
// Add some funds to the wallet from the faucet
|
|
219
|
+
await walletConnector.ensureBalance(nodeIdentity, addresses[0], 1000000000n);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
engineCore.logInfo(core.I18n.formatMessage("node.fundedWallet"));
|
|
223
|
+
}
|
|
224
|
+
return addresses;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Bootstrap the identity for the node.
|
|
228
|
+
* @param engineCore The engine core for the node.
|
|
229
|
+
* @param envVars The environment variables for the node.
|
|
230
|
+
* @param features The features that are enabled on the node.
|
|
231
|
+
* @param finalIdentity The identity of the node.
|
|
232
|
+
* @param addresses The addresses for the wallet.
|
|
233
|
+
*/
|
|
234
|
+
async function finaliseWallet(engineCore, envVars, features, finalIdentity, addresses) {
|
|
235
|
+
const engineDefaultTypes = engineCore.getDefaultTypes();
|
|
236
|
+
// If we are using entity storage for wallet the identity associated with the
|
|
237
|
+
// address will be wrong, so fix it
|
|
238
|
+
if (engineDefaultTypes.walletConnector === "entity-storage") {
|
|
239
|
+
const walletAddress = entityStorageModels.EntityStorageConnectorFactory.get(core.StringHelper.kebabCase("WalletAddress"));
|
|
240
|
+
const addr = await walletAddress.get(addresses[0]);
|
|
241
|
+
if (!core.Is.empty(addr)) {
|
|
242
|
+
addr.identity = finalIdentity;
|
|
243
|
+
await walletAddress.set(addr);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Generate a mnemonic for the node identity.
|
|
249
|
+
* @param engineCore The engine core for the node.
|
|
250
|
+
* @param envVars The environment variables for the node.
|
|
251
|
+
* @param features The features that are enabled on the node.
|
|
252
|
+
* @param vaultConnector The vault connector to use.
|
|
253
|
+
* @param nodeIdentity The identity of the node.
|
|
254
|
+
*/
|
|
255
|
+
async function bootstrapMnemonic(engineCore, envVars, features, vaultConnector, nodeIdentity) {
|
|
256
|
+
let mnemonic = envVars.mnemonic;
|
|
257
|
+
let storeMnemonic = false;
|
|
258
|
+
try {
|
|
259
|
+
const storedMnemonic = await vaultConnector.getSecret(`${nodeIdentity}/mnemonic`);
|
|
260
|
+
storeMnemonic = storedMnemonic !== mnemonic;
|
|
261
|
+
mnemonic = storedMnemonic;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
storeMnemonic = true;
|
|
265
|
+
}
|
|
266
|
+
// If there is no mnemonic then we need to generate one
|
|
267
|
+
if (core.Is.empty(mnemonic)) {
|
|
268
|
+
mnemonic = crypto.Bip39.randomMnemonic();
|
|
269
|
+
storeMnemonic = true;
|
|
270
|
+
engineCore.logInfo(core.I18n.formatMessage("node.generatingMnemonic", { mnemonic }));
|
|
271
|
+
}
|
|
272
|
+
// If there is no mnemonic stored in the vault then we need to store it
|
|
273
|
+
if (storeMnemonic) {
|
|
274
|
+
engineCore.logInfo(core.I18n.formatMessage("node.storingMnemonic"));
|
|
275
|
+
await vaultConnector.setSecret(`${nodeIdentity}/mnemonic`, mnemonic);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
engineCore.logInfo(core.I18n.formatMessage("node.existingMnemonic"));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Finalise the mnemonic for the node identity.
|
|
283
|
+
* @param vaultConnector The vault connector to use.
|
|
284
|
+
* @param workingIdentity The identity of the node.
|
|
285
|
+
* @param finalIdentity The final identity for the node.
|
|
286
|
+
*/
|
|
287
|
+
async function finaliseMnemonic(vaultConnector, workingIdentity, finalIdentity) {
|
|
288
|
+
// Now that we have an identity we can remove the temporary one
|
|
289
|
+
// and store the mnemonic with the new identity
|
|
290
|
+
if (workingIdentity.startsWith("bootstrap-temp-") && workingIdentity !== finalIdentity) {
|
|
291
|
+
const mnemonic = await vaultConnector.getSecret(`${workingIdentity}/mnemonic`);
|
|
292
|
+
await vaultConnector.setSecret(`${finalIdentity}/mnemonic`, mnemonic);
|
|
293
|
+
await vaultConnector.removeSecret(`${workingIdentity}/mnemonic`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Bootstrap the user.
|
|
298
|
+
* @param engineCore The engine core for the node.
|
|
299
|
+
* @param context The context for the node.
|
|
300
|
+
* @param envVars The environment variables for the node.
|
|
301
|
+
* @param features The features that are enabled on the node.
|
|
302
|
+
*/
|
|
303
|
+
async function bootstrapNodeUser(engineCore, context, envVars, features) {
|
|
304
|
+
if (features.includes(NodeFeatures.NodeUser)) {
|
|
305
|
+
const engineDefaultTypes = engineCore.getDefaultTypes();
|
|
306
|
+
if (engineDefaultTypes.authenticationComponent === "authentication-entity-storage" &&
|
|
307
|
+
core.Is.stringValue(context.state.nodeIdentity)) {
|
|
308
|
+
const authUserEntityStorage = entityStorageModels.EntityStorageConnectorFactory.get(core.StringHelper.kebabCase("AuthenticationUser"));
|
|
309
|
+
const email = envVars.username ?? DEFAULT_NODE_USERNAME;
|
|
310
|
+
let nodeAdminUser = await authUserEntityStorage.get(email);
|
|
311
|
+
const generatedPassword = envVars.password ?? crypto.PasswordGenerator.generate(16);
|
|
312
|
+
const passwordBytes = core.Converter.utf8ToBytes(generatedPassword);
|
|
313
|
+
if (core.Is.empty(nodeAdminUser)) {
|
|
314
|
+
engineCore.logInfo(core.I18n.formatMessage("node.creatingNodeUser"));
|
|
315
|
+
const saltBytes = core.RandomHelper.generate(16);
|
|
316
|
+
const hashedPassword = await apiAuthEntityStorageService.PasswordHelper.hashPassword(passwordBytes, saltBytes);
|
|
317
|
+
nodeAdminUser = {
|
|
318
|
+
email,
|
|
319
|
+
password: hashedPassword,
|
|
320
|
+
salt: core.Converter.bytesToBase64(saltBytes),
|
|
321
|
+
identity: context.state.nodeIdentity
|
|
322
|
+
};
|
|
323
|
+
engineCore.logInfo(core.I18n.formatMessage("node.nodeAdminUserEmail", { email: nodeAdminUser.email }));
|
|
324
|
+
engineCore.logInfo(core.I18n.formatMessage("node.nodeAdminUserPassword", { password: generatedPassword }));
|
|
325
|
+
await authUserEntityStorage.set(nodeAdminUser);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
engineCore.logInfo(core.I18n.formatMessage("node.existingNodeUser"));
|
|
329
|
+
// The user already exists, so double check the other details match
|
|
330
|
+
const saltBytes = core.Converter.base64ToBytes(nodeAdminUser.salt);
|
|
331
|
+
const hashedPassword = await apiAuthEntityStorageService.PasswordHelper.hashPassword(passwordBytes, saltBytes);
|
|
332
|
+
if (nodeAdminUser.identity !== context.state.nodeIdentity ||
|
|
333
|
+
nodeAdminUser.password !== hashedPassword) {
|
|
334
|
+
nodeAdminUser.password = hashedPassword;
|
|
335
|
+
nodeAdminUser.identity = context.state.nodeIdentity;
|
|
336
|
+
await authUserEntityStorage.set(nodeAdminUser);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// We have create a node user, now we need to create a profile for the user
|
|
340
|
+
const identityProfileConnector = identityModels.IdentityProfileConnectorFactory.get(engineDefaultTypes.identityProfileConnector);
|
|
341
|
+
if (identityProfileConnector) {
|
|
342
|
+
let userProfile;
|
|
343
|
+
try {
|
|
344
|
+
userProfile = await identityProfileConnector.get(context.state.nodeIdentity);
|
|
345
|
+
}
|
|
346
|
+
catch { }
|
|
347
|
+
if (core.Is.empty(userProfile)) {
|
|
348
|
+
engineCore.logInfo(core.I18n.formatMessage("node.creatingUserProfile"));
|
|
349
|
+
const publicProfile = {
|
|
350
|
+
"@context": "https://schema.org",
|
|
351
|
+
"@type": "Person",
|
|
352
|
+
name: "Node Administrator"
|
|
353
|
+
};
|
|
354
|
+
const privateProfile = {
|
|
355
|
+
"@context": "https://schema.org",
|
|
356
|
+
"@type": "Person",
|
|
357
|
+
givenName: "Node",
|
|
358
|
+
familyName: "Administrator",
|
|
359
|
+
email
|
|
360
|
+
};
|
|
361
|
+
await identityProfileConnector.create(context.state.nodeIdentity, publicProfile, privateProfile);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
engineCore.logInfo(core.I18n.formatMessage("node.existingUserProfile"));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Bootstrap the attestation verification methods.
|
|
372
|
+
* @param engineCore The engine core for the node.
|
|
373
|
+
* @param context The context for the node.
|
|
374
|
+
* @param envVars The environment variables for the node.
|
|
375
|
+
* @param features The features that are enabled on the node.
|
|
376
|
+
*/
|
|
377
|
+
async function bootstrapAttestationMethod(engineCore, context, envVars, features) {
|
|
378
|
+
if (core.Is.stringValue(context.state.nodeIdentity) &&
|
|
379
|
+
core.Is.arrayValue(context.config.types.identityConnector) &&
|
|
380
|
+
core.Is.stringValue(envVars.attestationVerificationMethodId)) {
|
|
381
|
+
const engineDefaultTypes = engineCore.getDefaultTypes();
|
|
382
|
+
const identityConnector = identityModels.IdentityConnectorFactory.get(engineDefaultTypes.identityConnector);
|
|
383
|
+
const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(engineDefaultTypes.identityResolverConnector);
|
|
384
|
+
const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
|
|
385
|
+
let createVm = true;
|
|
386
|
+
try {
|
|
387
|
+
identityModels.DocumentHelper.getVerificationMethod(identityDocument, `${identityDocument.id}#${envVars.attestationVerificationMethodId}`, "assertionMethod");
|
|
388
|
+
createVm = false;
|
|
389
|
+
}
|
|
390
|
+
catch { }
|
|
391
|
+
if (createVm) {
|
|
392
|
+
// Add attestation verification method to DID, the correct node context is now in place
|
|
393
|
+
// so the keys for the verification method will be stored correctly
|
|
394
|
+
engineCore.logInfo(core.I18n.formatMessage("node.addingAttestation"));
|
|
395
|
+
await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", envVars.attestationVerificationMethodId);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
engineCore.logInfo(core.I18n.formatMessage("node.existingAttestation"));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Bootstrap the immutable proof verification methods.
|
|
404
|
+
* @param engineCore The engine core for the node.
|
|
405
|
+
* @param context The context for the node.
|
|
406
|
+
* @param envVars The environment variables for the node.
|
|
407
|
+
* @param features The features that are enabled on the node.
|
|
408
|
+
*/
|
|
409
|
+
async function bootstrapImmutableProofMethod(engineCore, context, envVars, features) {
|
|
410
|
+
const engineDefaultTypes = engineCore.getDefaultTypes();
|
|
411
|
+
if (core.Is.stringValue(context.state.nodeIdentity) &&
|
|
412
|
+
core.Is.arrayValue(context.config.types.identityConnector) &&
|
|
413
|
+
core.Is.stringValue(envVars.immutableProofVerificationMethodId)) {
|
|
414
|
+
const identityConnector = identityModels.IdentityConnectorFactory.get(engineDefaultTypes.identityConnector);
|
|
415
|
+
const identityResolverConnector = identityModels.IdentityResolverConnectorFactory.get(engineDefaultTypes.identityResolverConnector);
|
|
416
|
+
const identityDocument = await identityResolverConnector.resolveDocument(context.state.nodeIdentity);
|
|
417
|
+
let createVm = true;
|
|
418
|
+
try {
|
|
419
|
+
identityModels.DocumentHelper.getVerificationMethod(identityDocument, `${identityDocument.id}#${envVars.immutableProofVerificationMethodId}`, "assertionMethod");
|
|
420
|
+
createVm = false;
|
|
421
|
+
}
|
|
422
|
+
catch { }
|
|
423
|
+
if (createVm) {
|
|
424
|
+
// Add AIG verification method to DID, the correct node context is now in place
|
|
425
|
+
// so the keys for the verification method will be stored correctly
|
|
426
|
+
engineCore.logInfo(core.I18n.formatMessage("node.addingImmutableProof"));
|
|
427
|
+
await identityConnector.addVerificationMethod(context.state.nodeIdentity, context.state.nodeIdentity, "assertionMethod", envVars.immutableProofVerificationMethodId);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
engineCore.logInfo(core.I18n.formatMessage("node.existingImmutableProof"));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Bootstrap the keys for blob encryption.
|
|
436
|
+
* @param engineCore The engine core for the node.
|
|
437
|
+
* @param context The context for the node.
|
|
438
|
+
* @param envVars The environment variables for the node.
|
|
439
|
+
* @param features The features that are enabled on the node.
|
|
440
|
+
*/
|
|
441
|
+
async function bootstrapBlobEncryption(engineCore, context, envVars, features) {
|
|
442
|
+
if ((core.Coerce.boolean(envVars.blobStorageEnableEncryption) ?? false) &&
|
|
443
|
+
core.Is.stringValue(context.state.nodeIdentity)) {
|
|
444
|
+
const engineDefaultTypes = engineCore.getDefaultTypes();
|
|
445
|
+
// Create a new key for encrypting blobs
|
|
446
|
+
const vaultConnector = vaultModels.VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
|
|
447
|
+
const keyName = `${context.state.nodeIdentity}/${envVars.blobStorageEncryptionKey}`;
|
|
448
|
+
let existingKey;
|
|
449
|
+
try {
|
|
450
|
+
existingKey = await vaultConnector.getKey(keyName);
|
|
451
|
+
}
|
|
452
|
+
catch { }
|
|
453
|
+
if (core.Is.empty(existingKey)) {
|
|
454
|
+
engineCore.logInfo(core.I18n.formatMessage("node.creatingBlobEncryptionKey"));
|
|
455
|
+
await vaultConnector.createKey(`${context.state.nodeIdentity}/${envVars.blobStorageEncryptionKey}`, vaultModels.VaultKeyType.ChaCha20Poly1305);
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
engineCore.logInfo(core.I18n.formatMessage("node.existingBlobEncryptionKey"));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Bootstrap the JWT signing key.
|
|
464
|
+
* @param engineCore The engine core for the node.
|
|
465
|
+
* @param context The context for the node.
|
|
466
|
+
* @param envVars The environment variables for the node.
|
|
467
|
+
* @param features The features that are enabled on the node.
|
|
468
|
+
*/
|
|
469
|
+
async function bootstrapAuth(engineCore, context, envVars, features) {
|
|
470
|
+
const engineDefaultTypes = engineCore.getDefaultTypes();
|
|
471
|
+
if (engineDefaultTypes.authenticationComponent === "authentication-entity-storage" &&
|
|
472
|
+
core.Is.stringValue(context.state.nodeIdentity)) {
|
|
473
|
+
// Create a new JWT signing key and a user login for the node
|
|
474
|
+
const vaultConnector = vaultModels.VaultConnectorFactory.get(engineDefaultTypes.vaultConnector);
|
|
475
|
+
const keyName = `${context.state.nodeIdentity}/${envVars.authSigningKeyId}`;
|
|
476
|
+
let existingKey;
|
|
477
|
+
try {
|
|
478
|
+
existingKey = await vaultConnector.getKey(keyName);
|
|
479
|
+
}
|
|
480
|
+
catch { }
|
|
481
|
+
if (core.Is.empty(existingKey)) {
|
|
482
|
+
engineCore.logInfo(core.I18n.formatMessage("node.creatingAuthKey"));
|
|
483
|
+
await vaultConnector.createKey(`${context.state.nodeIdentity}/${envVars.authSigningKeyId}`, vaultModels.VaultKeyType.Ed25519);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
engineCore.logInfo(core.I18n.formatMessage("node.existingAuthKey"));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Start the engine server.
|
|
493
|
+
* @param serverInfo The server information.
|
|
494
|
+
* @param envVars The environment variables.
|
|
495
|
+
* @param openApiSpecFile Path to the OpenAPI spec file.
|
|
496
|
+
* @param stateStorage The state storage.
|
|
497
|
+
* @param extendConfig Extends the engine configuration with any additional custom configuration.
|
|
498
|
+
* @returns The engine server.
|
|
499
|
+
*/
|
|
500
|
+
async function start(serverInfo, envVars, openApiSpecFile, stateStorage, extendConfig) {
|
|
501
|
+
envVars.storageFileRoot ??= "";
|
|
502
|
+
if ((envVars.entityStorageConnectorType === engineTypes.EntityStorageConnectorType.File ||
|
|
503
|
+
envVars.blobStorageConnectorType === engineTypes.BlobStorageConnectorType.File ||
|
|
504
|
+
core.Is.empty(stateStorage)) &&
|
|
505
|
+
!core.Is.stringValue(envVars.storageFileRoot)) {
|
|
506
|
+
throw new core.GeneralError("node", "storageFileRootNotSet");
|
|
507
|
+
}
|
|
508
|
+
// Build the engine configuration from the environment variables.
|
|
509
|
+
const engineConfig = engine.buildEngineConfiguration(envVars);
|
|
510
|
+
// Extend the engine configuration with a custom type.
|
|
511
|
+
if (core.Is.function(extendConfig)) {
|
|
512
|
+
await extendConfig(engineConfig);
|
|
513
|
+
}
|
|
514
|
+
// Build the server configuration from the environment variables.
|
|
515
|
+
const serverConfig = engineServer.buildEngineServerConfiguration(envVars, engineConfig, serverInfo, openApiSpecFile);
|
|
516
|
+
// Create the engine instance using file state storage and custom bootstrap.
|
|
517
|
+
const engine$1 = new engine.Engine({
|
|
518
|
+
config: { ...engineConfig, ...serverConfig },
|
|
519
|
+
stateStorage: stateStorage ?? new engineCore.FileStateStorage(envVars.stateFilename ?? ""),
|
|
520
|
+
customBootstrap: async (core, engineContext) => bootstrap(core, engineContext, envVars)
|
|
521
|
+
});
|
|
522
|
+
// Need to register the engine with the factory so that background tasks
|
|
523
|
+
// can clone it to spawn new instances.
|
|
524
|
+
engineModels.EngineCoreFactory.register("engine", () => engine$1);
|
|
525
|
+
// Construct the server with the engine.
|
|
526
|
+
const server = new engineServer.EngineServer({ engineCore: engine$1 });
|
|
527
|
+
// Start the server, which also starts the engine.
|
|
528
|
+
const canContinue = await server.start();
|
|
529
|
+
if (canContinue) {
|
|
530
|
+
return {
|
|
531
|
+
engine: engine$1,
|
|
532
|
+
server
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Copyright 2024 IOTA Stiftung.
|
|
538
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
539
|
+
/* eslint-disable no-console */
|
|
540
|
+
/**
|
|
541
|
+
* Run the TWIN Node server.
|
|
542
|
+
* @param options Optional options for the server.
|
|
543
|
+
* @param options.serverName Optional name of the server, defaults to "TWIN Node Server".
|
|
544
|
+
* @param options.serverVersion Optional version of the server, defaults to current version.
|
|
545
|
+
* @param options.envFilenames Additional environment variable filenames to load, defaults to .env.
|
|
546
|
+
* @param options.envPrefix Optional prefix for environment variables, defaults to "TWIN_NODE_".
|
|
547
|
+
* @param options.executionDirectory Optional directory to override the execution location, defaults to process directory.
|
|
548
|
+
* @param options.localesDirectory Optional directory to override the locales directory, defaults to the locales directory.
|
|
549
|
+
* @param options.openApiSpecFile Optional path to the OpenAPI spec file, defaults to docs/open-api/spec.json.
|
|
550
|
+
* @returns A promise that resolves when the server is started.
|
|
551
|
+
*/
|
|
552
|
+
async function run(options) {
|
|
553
|
+
try {
|
|
554
|
+
const serverInfo = {
|
|
555
|
+
name: options?.serverName ?? "TWIN Node Server",
|
|
556
|
+
version: options?.serverVersion ?? "0.0.1-next.1" // x-release-please-version
|
|
557
|
+
};
|
|
558
|
+
console.log(`\u001B[4m🌩️ ${serverInfo.name} v${serverInfo.version}\u001B[24m\n`);
|
|
559
|
+
const executionDirectory = options?.executionDirectory ?? getExecutionDirectory();
|
|
560
|
+
console.info("Execution Directory:", process.cwd());
|
|
561
|
+
await initialiseLocales(options?.localesDirectory ?? path.resolve(path.join(executionDirectory, "dist", "locales")));
|
|
562
|
+
if (core.Is.empty(options?.openApiSpecFile)) {
|
|
563
|
+
const specFile = path.resolve(path.join(executionDirectory, "docs", "open-api", "spec.json"));
|
|
564
|
+
console.info("Default OpenAPI Spec File:", specFile);
|
|
565
|
+
if (await fileExists(specFile)) {
|
|
566
|
+
options ??= {};
|
|
567
|
+
options.openApiSpecFile = specFile;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (core.Is.empty(options?.envFilenames)) {
|
|
571
|
+
const envFile = path.resolve(path.join(executionDirectory, ".env"));
|
|
572
|
+
console.info("Default Environment File:", envFile);
|
|
573
|
+
options ??= {};
|
|
574
|
+
options.envFilenames = [envFile];
|
|
575
|
+
}
|
|
576
|
+
dotenv__namespace.config({
|
|
577
|
+
path: options?.envFilenames
|
|
578
|
+
});
|
|
579
|
+
const envPrefix = options?.envPrefix ?? "TWIN_NODE_";
|
|
580
|
+
console.info("Environment Prefix:", envPrefix);
|
|
581
|
+
const envVars = core.EnvHelper.envToJson(process.env, envPrefix);
|
|
582
|
+
console.info();
|
|
583
|
+
const startResult = await start(serverInfo, envVars, options?.openApiSpecFile);
|
|
584
|
+
if (!core.Is.empty(startResult)) {
|
|
585
|
+
for (const signal of ["SIGHUP", "SIGINT", "SIGTERM"]) {
|
|
586
|
+
process.on(signal, async () => {
|
|
587
|
+
await startResult.server.stop();
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
console.error(core.ErrorHelper.formatErrors(err).join("\n"));
|
|
594
|
+
// eslint-disable-next-line unicorn/no-process-exit
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
exports.NodeFeatures = NodeFeatures;
|
|
600
|
+
exports.bootstrap = bootstrap;
|
|
601
|
+
exports.bootstrapAttestationMethod = bootstrapAttestationMethod;
|
|
602
|
+
exports.bootstrapAuth = bootstrapAuth;
|
|
603
|
+
exports.bootstrapBlobEncryption = bootstrapBlobEncryption;
|
|
604
|
+
exports.bootstrapImmutableProofMethod = bootstrapImmutableProofMethod;
|
|
605
|
+
exports.bootstrapNodeIdentity = bootstrapNodeIdentity;
|
|
606
|
+
exports.bootstrapNodeUser = bootstrapNodeUser;
|
|
607
|
+
exports.fileExists = fileExists;
|
|
608
|
+
exports.getExecutionDirectory = getExecutionDirectory;
|
|
609
|
+
exports.getFeatures = getFeatures;
|
|
610
|
+
exports.initialiseLocales = initialiseLocales;
|
|
611
|
+
exports.run = run;
|
|
612
|
+
exports.start = start;
|