@ethernity-cloud/sdk-js 1.4.0 → 1.5.1

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.
@@ -24,6 +24,7 @@ const contract_abi = require('./pox_abi');
24
24
  const image_registry_abi = require('./image_registry_abi');
25
25
  const {sha256} = require("./etny_crypto");
26
26
  const {generateCertFromMrEnclave} = require("./self_sign");
27
+ const esr_wallet = require("./esr_wallet");
27
28
 
28
29
  // Baked per network by CI (render_network.sh): "mainnet" | "testnet". On testnet
29
30
  // the enclave self-signs its cert from MR_ENCLAVE (non-CAS mode); on mainnet the
@@ -43,6 +44,10 @@ class EtnySecureLock {
43
44
 
44
45
  setInitializers() {
45
46
  this.is_valid_client_data = true;
47
+ // Testnet holds the self-derived identity key HERE, in memory only, and
48
+ // never writes it. Stays null on mainnet, where CAS injects the key into
49
+ // key_file and every consumer reads that path exactly as before.
50
+ this.identity_private_key_pem = null;
46
51
  this.key_file = "/private/__ENCLAVE_NAME_SECURELOCK__/key.pem";
47
52
  this.cert_file = "/app/__ENCLAVE_NAME_SECURELOCK__/cert.pem";
48
53
  this.pub_cert_file = "/app/__ENCLAVE_NAME_SECURELOCK__/enclave_pub_cert.pem";
@@ -230,7 +235,7 @@ class EtnySecureLock {
230
235
  const encryptedBase64 = await this.swiftStreamService.getFileContent(bucketName, objectName);
231
236
 
232
237
  const encryptedTuple = etny_crypto.encryptedDataFromBase64Json(encryptedBase64);
233
- const decryptedResult = etny_crypto.decrypt(this.key_file, encryptedTuple);
238
+ const decryptedResult = etny_crypto.decrypt(this.privateKeyMaterial(), encryptedTuple);
234
239
  return decryptedResult.toString();
235
240
  } catch (e) {
236
241
  console.log(`Failed to get ${objectName} file`);
@@ -282,37 +287,68 @@ class EtnySecureLock {
282
287
  }
283
288
 
284
289
  resetPrivateCertFile() {
285
- //console.log('Resetting the cert file');
290
+ // Wipes the CAS-injected key from disk after it has been loaded
291
+ // (mainnet PRODUCTION hygiene). On testnet the key was never written --
292
+ // it lives only in memory -- so there is nothing on disk to wipe and the
293
+ // truncate/read below would throw on a file that does not exist.
294
+ //
295
+ // The in-memory key is deliberately NOT cleared here: waitForPayloadAndInput
296
+ // still needs it later to decrypt the payload/input, which runs after
297
+ // create() has called this. Clearing it would break the task on testnet only.
298
+ if (this.identity_private_key_pem) {
299
+ return;
300
+ }
286
301
  try {
287
- // const fd = fs.openSync(this.private_key_file, 'w+');
288
302
  fs.truncate(this.key_file, 0, function () {
289
303
  //console.log('Private certificate file was cleaned.')
290
304
  });
291
- // fs.closeSync(fd);
292
305
  } catch (e) {
293
306
  //console.log('Error while removing the private key file content', e);
294
307
  }
295
- const content = fs.readFileSync(this.key_file, 'utf8');
296
- //console.log('cert file content', content, '##################');
308
+ try {
309
+ fs.readFileSync(this.key_file, 'utf8');
310
+ } catch (e) {
311
+ // Nothing to read back; the wipe above is what mattered.
312
+ }
297
313
  }
298
314
 
299
315
  // Testnet non-CAS mode: derive a deterministic self-signed cert/key from this
300
- // enclave's MR_ENCLAVE and write them to the paths the rest of the code reads
301
- // (key_file for decrypt, cert_file for savePublicCert). No-op on mainnet,
302
- // where CAS injects the cert/key into those same paths. Idempotent: the key
303
- // is a pure function of MR_ENCLAVE so it is identical across restarts.
316
+ // enclave's identity. The CERTIFICATE is written (it is public, and
317
+ // savePublicCert reads it back to publish it); the PRIVATE KEY is kept ONLY
318
+ // in memory and never touches the filesystem.
319
+ //
320
+ // Why memory-only: on testnet there is no CAS and no attestation, so nothing
321
+ // provisions or protects the key from outside the enclave. Not writing it at
322
+ // all removes every at-rest attack on it outright, instead of relying on the
323
+ // file protection being keyed by something trustworthy on this path.
324
+ //
325
+ // MAINNET IS UNCHANGED: this method returns immediately there, CAS still
326
+ // injects cert+key into key_file/cert_file, and every consumer still reads
327
+ // key_file exactly as before (this.identity_private_key_pem stays null, and
328
+ // privateKeyMaterial() falls back to the path).
329
+ //
330
+ // Idempotent: the key is a pure function of the enclave identity, so it is
331
+ // identical across restarts without needing to persist it.
304
332
  generateSelfSignedCertIfTestnet() {
305
333
  if (NETWORK_TYPE !== "testnet") return;
306
334
  console.log("[SecureLock] TESTNET: self-signing certificate from MR_ENCLAVE (no CAS) [TESTNET-INSECURE]");
307
335
  const {privateKeyPem, certPem, mrEnclaveHex} = generateCertFromMrEnclave("SecureLock Enclave");
308
336
  console.log(`[SecureLock] MR_ENCLAVE: ${mrEnclaveHex}`);
309
- for (const f of [this.key_file, this.cert_file, this.pub_cert_file]) {
337
+ for (const f of [this.cert_file, this.pub_cert_file]) {
310
338
  try { fs.mkdirSync(require('path').dirname(f), {recursive: true}); } catch (e) {}
311
339
  }
312
- fs.writeFileSync(this.key_file, privateKeyPem);
340
+ // Private key: memory only. Deliberately NOT written to this.key_file.
341
+ this.identity_private_key_pem = privateKeyPem;
313
342
  fs.writeFileSync(this.cert_file, certPem);
314
343
  }
315
344
 
345
+ // The enclave's identity private key, in whatever form this network provides
346
+ // it: the in-memory PEM on testnet, or the CAS-injected key_file path on
347
+ // mainnet. etny_crypto accepts either, so callers do not branch on network.
348
+ privateKeyMaterial() {
349
+ return this.identity_private_key_pem || this.key_file;
350
+ }
351
+
316
352
  async savePublicCert() {
317
353
  try {
318
354
  const content = await fs.promises.readFile(this.cert_file, 'utf8');
@@ -321,12 +357,46 @@ class EtnySecureLock {
321
357
  console.log(`PUBLIC_KEY:`);
322
358
  console.log(content);
323
359
 
360
+ // ESR (RFC §5.2): emit the enclave's ESR wallet ADDRESS on the same
361
+ // harvest channel as the cert, so publish (or the remote extraction
362
+ // service) can capture it. It MUST be derived here rather than by the
363
+ // service: on mainnet the identity key is CAS-provisioned and exists
364
+ // only inside this enclave, so nobody outside can compute the address.
365
+ // Only the address is emitted — the private key never leaves.
366
+ this.emitEsrWalletAddress();
367
+
324
368
  await this.swiftStreamService.putFileContent(this.etny_bucket, 'cert.pem', this.pub_cert_file);
325
369
  } catch (err) {
326
370
  console.error(err);
327
371
  }
328
372
  }
329
373
 
374
+ // ESR wallet address emission (RFC §5.2). Gated on ESR_CONTRACT_ADDRESS being
375
+ // set, exactly like the Python securelock, so non-ESR images print nothing.
376
+ // Derived from the enclave's own identity private key (the certificate key);
377
+ // only the ADDRESS is logged, never the key. Any failure is reported as
378
+ // ESR_WALLET_ERROR and never breaks cert harvest or a task.
379
+ emitEsrWalletAddress() {
380
+ if (!String(process.env.ESR_CONTRACT_ADDRESS || "").trim()) return;
381
+ try {
382
+ // The identity key the enclave actually holds: the CAS-provisioned
383
+ // key_file on mainnet (nothing in-enclave can re-derive it), or the
384
+ // in-memory PEM on testnet (never written to disk). Hashed as-is,
385
+ // matching the Python SDK.
386
+ const identityPriv = this.identity_private_key_pem
387
+ || fs.readFileSync(this.key_file, 'utf8');
388
+ const esrAddress = esr_wallet.deriveWalletAddress(identityPriv);
389
+ console.log(`ESR_WALLET_ADDRESS: ${esrAddress}`);
390
+ if (!esr_wallet.isSecretIdentity(NETWORK_TYPE)) {
391
+ console.warn(esr_wallet.INSECURE_IDENTITY_WARNING);
392
+ }
393
+ } catch (e) {
394
+ // Never let ESR derivation break harvest/a task: report it the way
395
+ // every other enclave-side config problem is reported.
396
+ console.error(`ESR_WALLET_ERROR: ${e && e.message ? e.message : e}`);
397
+ }
398
+ }
399
+
330
400
  async saveResult() {
331
401
  try {
332
402
  await this.getTrustedZonePublicKey();
@@ -342,6 +412,37 @@ class EtnySecureLock {
342
412
  }
343
413
  }
344
414
 
415
+ // Make StateRegistry usable from payload code, if ESR is enabled.
416
+ //
417
+ // Wires ecld_state to this enclave's identity key, the SwiftStream client
418
+ // and bucket the node shares with us, the registry address baked in at build
419
+ // time, and the provider. Payload code then just does:
420
+ //
421
+ // const {StateRegistry} = require('./ecld_state');
422
+ // await new StateRegistry().commit('key', s => ...);
423
+ //
424
+ // No-op when ESR is disabled, so non-ESR enclaves are unaffected. Never
425
+ // throws: a setup problem must not stop the task from running -- payload
426
+ // code that then uses StateRegistry gets a clear error from the registry
427
+ // itself rather than a task that died before starting.
428
+ configureStateRegistry() {
429
+ const contractAddress = String(process.env.ESR_CONTRACT_ADDRESS || "").trim();
430
+ if (!contractAddress) return;
431
+ try {
432
+ const ecld_state = require("./ecld_state");
433
+ ecld_state.configure({
434
+ identityPriv: this.identity_private_key_pem || fs.readFileSync(this.key_file, 'utf8'),
435
+ swiftStreamService: this.swiftStreamService,
436
+ bucket: this.etny_bucket,
437
+ contractAddress: contractAddress,
438
+ provider: this.w3,
439
+ });
440
+ console.log(`StateRegistry ready (ESR ${contractAddress})`);
441
+ } catch (e) {
442
+ console.error(`ESR_STATE_ERROR: could not configure StateRegistry: ${e && e.message ? e.message : e}`);
443
+ }
444
+ }
445
+
345
446
  async execute() {
346
447
  const payload_data = await this.getFileContentAndDecrypt(this.etny_bucket, this.payload);
347
448
  const input_data = await this.getFileContentAndDecrypt(this.etny_bucket, this.input);
@@ -355,6 +456,8 @@ class EtnySecureLock {
355
456
  return false;
356
457
  }
357
458
 
459
+ this.configureStateRegistry();
460
+
358
461
  const task_result = executeTask(payload_data, input_data);
359
462
  console.log('TASK RESULT (code, data):', task_result);
360
463
  this.task_code = task_result[0];
@@ -378,6 +481,40 @@ function initSwiftStreamClient() {
378
481
  return swiftStreamClient;
379
482
  }
380
483
 
484
+ // Set as soon as the enclave object exists so the top-level catch can report a
485
+ // failure result even when the throw happens deep inside execute().
486
+ let currentApp = null;
487
+
488
+ /**
489
+ * Best-effort: write a failure result so the trustedzone stops waiting.
490
+ *
491
+ * Securelock only publishes the result and its code; the trustedzone reads
492
+ * those and builds the signed transaction. Without them that wait runs to its
493
+ * timeout and the whole order stalls with no diagnosis.
494
+ *
495
+ * Guarded throughout: this runs while already handling a failure, so it must
496
+ * never throw or hang.
497
+ */
498
+ async function reportFailure(app, err) {
499
+ if (!app) {
500
+ console.log('cannot report failure: enclave died before initialization');
501
+ return;
502
+ }
503
+ try {
504
+ // Keep a specific code if one was already set (checksum mismatch, ...).
505
+ if (app.task_code === undefined || app.task_code === null || app.task_code === '') {
506
+ app.task_code = TaskStatus.SYSTEM_ERROR;
507
+ }
508
+ if (!app.task_result) {
509
+ app.task_result = `ENCLAVE ERROR: ${err && err.message ? err.message : err}`;
510
+ }
511
+ await app.saveResult();
512
+ console.log(`failure reported (task_code=${app.task_code})`);
513
+ } catch (reportErr) {
514
+ console.log('could not report the failure result:', reportErr);
515
+ }
516
+ }
517
+
381
518
  async function execute() {
382
519
  console.log('########################################');
383
520
  console.log('########################################');
@@ -391,6 +528,9 @@ async function execute() {
391
528
  const swiftStreamClient = initSwiftStreamClient();
392
529
 
393
530
  const app = new EtnySecureLock(swiftStreamClient);
531
+ // Exposed so the top-level handler can still publish a failure result if
532
+ // anything below throws (see reportFailure).
533
+ currentApp = app;
394
534
 
395
535
  // Testnet: materialize the self-signed cert/key from MR_ENCLAVE into the
396
536
  // file paths before anything reads them (harvest + decrypt). No-op on mainnet
@@ -440,5 +580,7 @@ async function execute() {
440
580
  await execute();
441
581
  } catch (e) {
442
582
  console.log(e);
583
+ await reportFailure(currentApp, e);
584
+ process.exitCode = 1;
443
585
  }
444
586
  })();
@@ -11,6 +11,10 @@ class TaskStatus {
11
11
  // Extended diagnostic (matches the trustedzone's extended enum): the
12
12
  // serverless backend failed to load, so none of its functions exist.
13
13
  static IMPORT_ERROR = 28;
14
+ // A required enclave config value is present but EMPTY (e.g. an ESR
15
+ // address that was never baked into the sealed image). Reported eagerly
16
+ // with the variable named, instead of a confusing downstream crash.
17
+ static CONFIG_ERROR = 32;
14
18
  }
15
19
 
16
20
  module.exports = {
@@ -156,9 +156,27 @@ if (!fs.existsSync('src/sgx_report.node')) {
156
156
  console.error(' securelock/src/ (see src/README-keygen.md) before building.');
157
157
  process.exit(1);
158
158
  }
159
+ // ESR fail-fast gate (RFC §5.4), mirroring the Python SDK's validate_esr_config:
160
+ // never build an ESR-enabled enclave with an unresolved registry address. The
161
+ // enclave is SEALED -- a missing value bakes in as empty, and every task then
162
+ // fails at runtime after gas is already spent. ESR is opt-in: absent/false
163
+ // ESR_ENABLED means the build behaves exactly as it did before ESR existed.
164
+ const ESR_ENABLED = /^(1|true|yes)$/i.test(String(process.env.ESR_ENABLED || '').trim());
165
+ const ESR_CONTRACT_ADDRESS = String(process.env.ESR_CONTRACT_ADDRESS || '').trim();
166
+ if (ESR_ENABLED && !/^0x[0-9a-fA-F]{40}$/.test(ESR_CONTRACT_ADDRESS)) {
167
+ console.error('ERROR: ESR is enabled but ESR_CONTRACT_ADDRESS is not a valid address'
168
+ + ` (got ${JSON.stringify(ESR_CONTRACT_ADDRESS)}).`);
169
+ console.error(' The enclave is sealed: a missing value bakes in as EMPTY and every');
170
+ console.error(' task fails at runtime after gas is spent. Set it with:');
171
+ console.error(' ecld-init (ESR step) or ESR_CONTRACT_ADDRESS in .env');
172
+ process.exit(1);
173
+ }
174
+ // Rendered only when enabled, so non-ESR images keep byte-identical layers.
175
+ const esrEnvBlock = ESR_ENABLED ? `ENV ESR_CONTRACT_ADDRESS=${ESR_CONTRACT_ADDRESS}` : '';
176
+
159
177
  // runCommand(`cat Dockerfile.tmpl | sed s/"__ENCLAVE_NAME_SECURELOCK__"/"${ENCLAVE_NAME_SECURELOCK}"/g > Dockerfile`);
160
178
  const dockerfileSecureTemplate = fs.readFileSync('Dockerfile.tmpl', 'utf8');
161
- let dockerfileSecureContent = dockerfileSecureTemplate.replace(/__ENCLAVE_NAME_SECURELOCK__/g, ENCLAVE_NAME_SECURELOCK).replace(/__BUCKET_NAME__/g, templateName + "-v3").replace(/__SMART_CONTRACT_ADDRESS__/g, ECRunner[templateName][0]).replace(/__IMAGE_REGISTRY_ADDRESS__/g, ECRunner[templateName][1]).replace(/__RPC_URL__/g, ECRunner[templateName][2]).replace(/__CHAIN_ID__/g, ECRunner[templateName][3]).replace(/__TRUSTED_ZONE_IMAGE__/g, templateName);
179
+ let dockerfileSecureContent = dockerfileSecureTemplate.replace(/__ENCLAVE_NAME_SECURELOCK__/g, ENCLAVE_NAME_SECURELOCK).replace(/__BUCKET_NAME__/g, templateName + "-v3").replace(/__SMART_CONTRACT_ADDRESS__/g, ECRunner[templateName][0]).replace(/__IMAGE_REGISTRY_ADDRESS__/g, ECRunner[templateName][1]).replace(/__RPC_URL__/g, ECRunner[templateName][2]).replace(/__CHAIN_ID__/g, ECRunner[templateName][3]).replace(/__TRUSTED_ZONE_IMAGE__/g, templateName).replace(/^__ESR_ENV__\n/m, esrEnvBlock ? `${esrEnvBlock}\n` : '');
162
180
 
163
181
  // Amount of enclave heap to allocate (SCONE_HEAP). Kept in sync with the
164
182
  // run/docker-compose securelock service; overridable via ECLD_MEMORY_TO_ALLOCATE.
@@ -78,6 +78,17 @@ console.log("Docker Composer Hash:", dockerComposerHash);
78
78
  console.log("Public Key:", checkResponse.publicKey);
79
79
  // Save public key to file
80
80
  fs.writeFileSync('PUBLIC_KEY.txt', checkResponse.publicKey);
81
+ // ESR (RFC §5.2): the enclave also prints ESR_WALLET_ADDRESS on the
82
+ // cert channel. The service currently returns only "publicKey"; when
83
+ // it starts relaying the enclave's output (or adds an
84
+ // "esrWalletAddress" field) it is picked up here and written to a
85
+ // sibling file the caller reads -- no other changes needed. Until
86
+ // then, ESR projects extract locally on an SGX host once to learn
87
+ // the address (publish then persists it).
88
+ if (checkResponse.esrWalletAddress) {
89
+ console.log("ESR Wallet Address:", checkResponse.esrWalletAddress);
90
+ fs.writeFileSync('ESR_WALLET_ADDRESS.txt', checkResponse.esrWalletAddress);
91
+ }
81
92
  break;
82
93
  }
83
94
  } else {
package/nodenithy/run.js CHANGED
@@ -497,6 +497,28 @@ const main = async () => {
497
497
  console.error("Error: Could not fetch PUBLIC_KEY_SECURELOCK");
498
498
  process.exit(1);
499
499
  }
500
+ // ESR (RFC §5.2): if the extraction relayed the enclave's ESR wallet
501
+ // address (public_key_service writes ESR_WALLET_ADDRESS.txt when the
502
+ // service provides it), persist it to .env so a dApp can read/fund the
503
+ // enclave wallet. Absent file => not an ESR project, or the (mainnet)
504
+ // address must be learned from a local SGX extraction — either way,
505
+ // silently skip. Parity with the Python SDK's esr.wallet_address.
506
+ try {
507
+ if (fs.existsSync('ESR_WALLET_ADDRESS.txt')) {
508
+ const esrAddr = fs.readFileSync('ESR_WALLET_ADDRESS.txt', 'utf8').trim();
509
+ if (/^0x[0-9a-fA-F]{40}$/.test(esrAddr)) {
510
+ writeEnv('ESR_WALLET_ADDRESS', esrAddr);
511
+ console.log(`ESR_WALLET_ADDRESS: ${esrAddr}`);
512
+ // Manual funding by the data owner is the default path;
513
+ // publishing never transfers value on its own. Say so, or
514
+ // the first task needing gas fails with no clue why.
515
+ console.log(' This wallet starts EMPTY. Fund it from your own wallet with');
516
+ console.log(' whatever your payload needs; publishing never transfers value.');
517
+ }
518
+ }
519
+ } catch (e) {
520
+ console.warn(`Could not persist ESR_WALLET_ADDRESS: ${e && e.message ? e.message : e}`);
521
+ }
500
522
  };
501
523
  }
502
524
 
package/package.json CHANGED
@@ -1,50 +1,50 @@
1
- {
2
- "name": "@ethernity-cloud/sdk-js",
3
- "version": "1.4.0",
4
- "description": "A package to run ethernity cloud build scripts",
5
- "scripts": {
6
- "ecld-build": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/build.js",
7
- "ecld-publish": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/publish.js",
8
- "ecld-init": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/init.js",
9
- "ecld-run": "cross-env ecld-run",
10
- "postinstall": "node postinstall.js",
11
- "ecld-test": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/test.js"
12
- },
13
- "bin": {
14
- "ecld-build": "./build.js",
15
- "ecld-publish": "./publish.js",
16
- "ecld-init": "./init.js",
17
- "postinstall": "./postinstall.js",
18
- "ecld-test": "./test.js"
19
- },
20
- "files": [
21
- "nodenithy/*",
22
- "pynithy/*",
23
- "build.js",
24
- "init.js",
25
- "publish.js",
26
- "postinstall.js"
27
- ],
28
- "dependencies": {
29
- "@ethernity-cloud/runner": "^0.0.26",
30
- "axios": "^1.7.2",
31
- "commander": "^12.1.0",
32
- "dotenv": "^16.4.5",
33
- "eth-lib": "^0.1.29",
34
- "ethers": "^5.5.4",
35
- "kubo-rpc-client": "^4.1.1",
36
- "web3": "^4.9.0",
37
- "cross-env": "^7.0.3",
38
- "shelljs": "^0.8.5",
39
- "shx": "^0.3.4",
40
- "node-forge": "^1.3.1",
41
- "cli-progress": "^3.12.0",
42
- "react": "^18.3.1",
43
- "react-dom": "^18.3.1",
44
- "react-scripts": "^5.0.1",
45
- "web-vitals": "^2.1.4",
46
- "adm-zip": "^0.5.14"
47
- },
48
- "author": "Alexandru Luca",
49
- "license": "AGPL-3.0"
50
- }
1
+ {
2
+ "name": "@ethernity-cloud/sdk-js",
3
+ "version": "1.5.1",
4
+ "description": "A package to run ethernity cloud build scripts",
5
+ "scripts": {
6
+ "ecld-build": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/build.js",
7
+ "ecld-publish": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/publish.js",
8
+ "ecld-init": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/init.js",
9
+ "ecld-run": "cross-env ecld-run",
10
+ "postinstall": "node postinstall.js",
11
+ "ecld-test": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/test.js"
12
+ },
13
+ "bin": {
14
+ "ecld-build": "./build.js",
15
+ "ecld-publish": "./publish.js",
16
+ "ecld-init": "./init.js",
17
+ "postinstall": "./postinstall.js",
18
+ "ecld-test": "./test.js"
19
+ },
20
+ "files": [
21
+ "nodenithy/*",
22
+ "pynithy/*",
23
+ "build.js",
24
+ "init.js",
25
+ "publish.js",
26
+ "postinstall.js"
27
+ ],
28
+ "dependencies": {
29
+ "@ethernity-cloud/runner": "^0.0.26",
30
+ "axios": "^1.7.2",
31
+ "commander": "^12.1.0",
32
+ "dotenv": "^16.4.5",
33
+ "eth-lib": "^0.1.29",
34
+ "ethers": "^5.5.4",
35
+ "kubo-rpc-client": "^4.1.1",
36
+ "web3": "^4.9.0",
37
+ "cross-env": "^7.0.3",
38
+ "shelljs": "^0.8.5",
39
+ "shx": "^0.3.4",
40
+ "node-forge": "^1.3.1",
41
+ "cli-progress": "^3.12.0",
42
+ "react": "^18.3.1",
43
+ "react-dom": "^18.3.1",
44
+ "react-scripts": "^5.0.1",
45
+ "web-vitals": "^2.1.4",
46
+ "adm-zip": "^0.5.14"
47
+ },
48
+ "author": "Alexandru Luca",
49
+ "license": "AGPL-3.0"
50
+ }