@ethernity-cloud/sdk-js 1.7.8 → 1.7.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.
package/nodenithy/run.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const fs = require('fs');
2
- const { execSync } = require('child_process');
2
+ const { execSync, execFileSync } = require('child_process');
3
3
  require('dotenv').config();
4
4
  const readline = require('readline');
5
5
  const forge = require('node-forge');
@@ -8,7 +8,7 @@ const https = require('https');
8
8
  const casConfig = require('../cas/config.js');
9
9
  const casResolver = require('../cas/resolver.js');
10
10
  const sessionRegistry = require('../cas/session_registry.js');
11
- const { LocalKubo, ownEndpoint } = require('../localKubo.js');
11
+ const { LocalKubo, ownEndpoint, PUBLIC_IPFS_HOST } = require('../localKubo.js');
12
12
 
13
13
  if (!fs.existsSync('.env')) {
14
14
  console.error("Error: .env file not found");
@@ -121,13 +121,17 @@ const main = async () => {
121
121
  const trustedZoneHeap = isMainnet ? '256M' : '1024M';
122
122
 
123
123
  // Where the image goes. An IPFS API of the application's own
124
- // (IPFS_ENDPOINT, when it is not the public one) takes precedence.
125
- // Otherwise the publish runs its own Kubo: the public write API of
126
- // ipfs.ethernity.cloud is closing, and the image is held by this Kubo,
127
- // peered with the bootnode, until its certificate is on chain.
124
+ // (IPFS_ENDPOINT, with IPFS_TOKEN as its Authorization header when set)
125
+ // takes precedence. Otherwise the publish runs its own Kubo: the public
126
+ // API of ipfs.ethernity.cloud takes no adds or pins without a writer
127
+ // token, and the image is held by this Kubo, peered with the bootnode,
128
+ // until its certificate is on chain.
128
129
  let localKubo = null;
129
130
  let ipfsApi = process.env.IPFS_ENDPOINT || '';
130
- if (!ownEndpoint(ipfsApi)) {
131
+ let ipfsToken = '';
132
+ if (ownEndpoint(ipfsApi)) {
133
+ ipfsToken = process.env.IPFS_TOKEN || '';
134
+ } else {
131
135
  localKubo = new LocalKubo(process.env.PROJECT_NAME);
132
136
  try {
133
137
  await localKubo.start();
@@ -139,23 +143,28 @@ const main = async () => {
139
143
  console.log(`\t✔ IPFS node for this publish: ${ipfsApi}`);
140
144
  process.on('exit', () => localKubo.release());
141
145
  }
146
+ // node ../ipfs.mjs against ipfsApi, with the token as IPFS_TOKEN in its
147
+ // environment: the command line is in a failure's message and in the
148
+ // process list. A non-zero exit ends the publish.
149
+ const ipfsEnv = { ...process.env, IPFS_TOKEN: ipfsToken };
150
+ const ipfsCli = (args, stdio) => {
151
+ try {
152
+ return execFileSync(process.execPath, ['../ipfs.mjs', '--host', ipfsApi, ...args], { stdio, env: ipfsEnv });
153
+ } catch (e) {
154
+ console.error(`Error: node ../ipfs.mjs ${args.join(' ')} failed (${e.status != null ? `exit ${e.status}` : (e.signal || e.code)}).`);
155
+ process.exit(1);
156
+ }
157
+ };
142
158
 
143
159
  // The CIDs uploadToIpfs gives, from the same add calls with only-hash: the
144
160
  // IPFS node stores and announces nothing. Set by hashForIpfs, after which
145
161
  // uploadToIpfs refuses an upload that gives other CIDs.
146
162
  let expectedCids = null;
147
163
  const hashForIpfs = () => {
148
- const hashOf = (target) => {
149
- try {
150
- const out = execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action hash ${target}`, { stdio: ['ignore', 'pipe', 'inherit'] });
151
- return out.toString().trim().split('\n').pop().trim();
152
- } catch (e) {
153
- console.error(`Error: could not hash ${target} for IPFS: ${e.message}`);
154
- process.exit(1);
155
- }
156
- };
157
- process.env.IPFS_DOCKER_COMPOSE_HASH = hashOf('--filePath docker-compose-final.yml');
158
- process.env.IPFS_HASH = hashOf(`--folderPath ${registryPath}`);
164
+ const hashOf = (target) => ipfsCli(['--action', 'hash', ...target], ['ignore', 'pipe', 'inherit'])
165
+ .toString().trim().split('\n').pop().trim();
166
+ process.env.IPFS_DOCKER_COMPOSE_HASH = hashOf(['--filePath', 'docker-compose-final.yml']);
167
+ process.env.IPFS_HASH = hashOf(['--folderPath', registryPath]);
159
168
  console.log("IPFS_DOCKER_COMPOSE_HASH (before upload): ", process.env.IPFS_DOCKER_COMPOSE_HASH);
160
169
  console.log("IPFS_HASH (before upload): ", process.env.IPFS_HASH);
161
170
  expectedCids = { image: process.env.IPFS_HASH, compose: process.env.IPFS_DOCKER_COMPOSE_HASH };
@@ -168,7 +177,7 @@ const main = async () => {
168
177
  if (fs.existsSync(file)) fs.unlinkSync(file);
169
178
  }
170
179
  console.log('Upload docker-compose-final.yml to IPFS');
171
- execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action upload --filePath docker-compose-final.yml`, { stdio: "inherit" });
180
+ ipfsCli(['--action', 'upload', '--filePath', 'docker-compose-final.yml'], 'inherit');
172
181
  if (!fs.existsSync('IPFS_DOCKER_COMPOSE_HASH.ipfs')) {
173
182
  console.error("Error: Could not upload docker-compose-final.yml to IPFS, please try again!");
174
183
  process.exit(1);
@@ -177,7 +186,7 @@ const main = async () => {
177
186
  console.log("IPFS_DOCKER_COMPOSE_HASH: ", process.env.IPFS_DOCKER_COMPOSE_HASH);
178
187
  writeEnv('IPFS_DOCKER_COMPOSE_HASH', process.env.IPFS_DOCKER_COMPOSE_HASH);
179
188
  console.log('Upload docker registry to IPFS');
180
- execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action upload --folderPath ${registryPath}`, { stdio: "inherit" });
189
+ ipfsCli(['--action', 'upload', '--folderPath', registryPath], 'inherit');
181
190
  if (!fs.existsSync('IPFS_HASH.ipfs')) {
182
191
  console.error("Error: Could not upload registry to IPFS, please try again!");
183
192
  process.exit(1);
@@ -324,7 +333,7 @@ const main = async () => {
324
333
  const registered = await sessionRegistry.register(
325
334
  casChain.rpcUrl, casChain.chainId, casConfig.SESSION_REGISTRY[templateName],
326
335
  process.env.PRIVATE_KEY, fs.readFileSync('etny-securelock-test.yaml'),
327
- ipfsApi, '');
336
+ ipfsApi, '', ipfsToken);
328
337
  if (localKubo) await localKubo.provide(registered.cid);
329
338
  if (registered.registered) {
330
339
  console.log(`\t✔ Session ${registered.name} registered on-chain (body ${registered.cid})`);
@@ -589,6 +598,29 @@ const main = async () => {
589
598
  if (registryV2 && localKubo) {
590
599
  await localKubo.provide(process.env.IPFS_HASH);
591
600
  await localKubo.provide(process.env.IPFS_DOCKER_COMPOSE_HASH);
601
+ } else if (!registryV2 && localKubo) {
602
+ // On a V1 registry nothing is recorded before the certificate, so the
603
+ // bootnode's mirror pins nothing from this Kubo, and the certificate
604
+ // extraction service and the nodes, which fetch from the bootnode,
605
+ // would wait on an image it never receives: the compose and the image
606
+ // are pinned on the public API, which fetches them from this Kubo over
607
+ // the connection it holds. The pin takes a token (IPFS_TOKEN); without
608
+ // one the publish stops here, before anything is on chain.
609
+ const publicToken = process.env.IPFS_TOKEN || '';
610
+ if (!publicToken) {
611
+ console.error("Error: this network's image registry records an image only with its certificate, so the image must be pinned on the public IPFS API before the certificate is extracted, and the public API takes a pin only with a token.");
612
+ console.error(` Set IPFS_TOKEN to a writer token of ${PUBLIC_IPFS_HOST} and publish again.`);
613
+ process.exit(1);
614
+ }
615
+ for (const cid of [process.env.IPFS_DOCKER_COMPOSE_HASH, process.env.IPFS_HASH]) {
616
+ console.log(`Pinning ${cid} on the public IPFS API`);
617
+ try {
618
+ execFileSync(process.execPath, ['../ipfs.mjs', '--host', `https://${PUBLIC_IPFS_HOST}`, '--action', 'pin', '--hhash', cid], { stdio: 'inherit', env: { ...process.env, IPFS_TOKEN: publicToken } });
619
+ } catch (e) {
620
+ console.error(`Error: the public IPFS API did not pin ${cid} (${e.status != null ? `exit ${e.status}` : (e.signal || e.code)}).`);
621
+ process.exit(1);
622
+ }
623
+ }
592
624
  }
593
625
  if (casTestnet) await registerSessionOnChain();
594
626
 
@@ -722,7 +754,7 @@ const main = async () => {
722
754
  fs.unlinkSync('IPFS_HASH.ipfs');
723
755
  }
724
756
  console.log('Upload docker registry to IPFS');
725
- execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action upload --folderPath ${registryPath}`, { stdio: "inherit" });
757
+ ipfsCli(['--action', 'upload', '--folderPath', registryPath], 'inherit');
726
758
  if (!fs.existsSync(`./IPFS_HASH.ipfs`)) {
727
759
  console.error("Error: Could not upload registry to IPFS, please try again!");
728
760
  process.exit(1);
@@ -5,7 +5,8 @@ import { ECStatus } from "@ethernity-cloud/runner/enums";
5
5
  // Filled in from the project's .env by src/preStart.mjs on `npm start`: the
6
6
  // securelock this dApp is published as, the trustedzone it was built against,
7
7
  // the network's token address and chain id, the -unsafe network when the dApp
8
- // runs without a CAS (null otherwise), and the IPFS endpoint.
8
+ // runs without a CAS (null otherwise), and the IPFS API of the dApp's own ('' when
9
+ // the task's artefacts go through the bootnode's payload intake).
9
10
  const PROJECT_NAME = "";
10
11
  const TRUSTED_ZONE_IMAGE = "";
11
12
  const NETWORK_ADDRESS = "";
@@ -23,7 +24,10 @@ function App() {
23
24
  if (UNSAFE_NETWORK) {
24
25
  await runner.setNetwork(...UNSAFE_NETWORK);
25
26
  }
26
- runner.initializeStorage(IPFS_ENDPOINT);
27
+ // An IPFS API of the dApp's own (IPFS_ENDPOINT) carries the task's
28
+ // artefacts; without one they go through the bootnode's payload intake.
29
+ if (IPFS_ENDPOINT) runner.initializeStorage(IPFS_ENDPOINT);
30
+ else runner.initializePublicIntake();
27
31
 
28
32
  // Events are named by task status; detail is { message, status, progress }.
29
33
  runner.addEventListener(ECStatus.DEFAULT, (e) => {
@@ -3,7 +3,10 @@
3
3
  // .env, the way ecld-run reads the same values. PROJECT_NAME and
4
4
  // TRUSTED_ZONE_IMAGE take their -unsafe form on an -unsafe network; the
5
5
  // network's token address, chain id and -unsafe (network, type) come from the
6
- // runner's own enums through the SDK's network table.
6
+ // runner's own enums through the SDK's network table; IPFS_ENDPOINT is the
7
+ // IPFS API of the dApp's own, or '' (no setting, or the public API, which
8
+ // takes no writes without a token), on which the dApp delivers its task
9
+ // artefacts through the bootnode's payload intake.
7
10
  import fs from 'fs';
8
11
  import { createRequire } from 'module';
9
12
  import dotenv from 'dotenv';
@@ -13,6 +16,7 @@ dotenv.config();
13
16
  const require = createRequire(import.meta.url);
14
17
  const casConfig = require('@ethernity-cloud/sdk-js/cas/config.js');
15
18
  const { networkFor } = require('@ethernity-cloud/sdk-js/network.js');
19
+ const { dappEndpoint } = require('@ethernity-cloud/sdk-js/localKubo.js');
16
20
 
17
21
  const { PROJECT_NAME, TRUSTED_ZONE_IMAGE, BLOCKCHAIN_NETWORK, IPFS_ENDPOINT } = process.env;
18
22
  if (!PROJECT_NAME || !BLOCKCHAIN_NETWORK) {
@@ -31,7 +35,7 @@ const constants = {
31
35
  NETWORK_ADDRESS: network.address,
32
36
  CHAIN_ID: network.chainId || null,
33
37
  UNSAFE_NETWORK: network.unsafe || null,
34
- IPFS_ENDPOINT: IPFS_ENDPOINT || 'https://ipfs.ethernity.cloud',
38
+ IPFS_ENDPOINT: dappEndpoint(IPFS_ENDPOINT),
35
39
  };
36
40
 
37
41
  const filePath = 'src/ec_helloworld_example.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethernity-cloud/sdk-js",
3
- "version": "1.7.8",
3
+ "version": "1.7.10",
4
4
  "description": "A package to run ethernity cloud build scripts",
5
5
  "scripts": {
6
6
  "test": "node --test \"test/**/*.test.js\"",
@@ -39,7 +39,7 @@
39
39
  "!**/*.pyc"
40
40
  ],
41
41
  "dependencies": {
42
- "@ethernity-cloud/runner": "^0.6.3",
42
+ "@ethernity-cloud/runner": "^0.6.5",
43
43
  "acorn": "^8.11.0",
44
44
  "adm-zip": "^0.5.14",
45
45
  "axios": "^1.7.2",
package/pynithy/build.mjs CHANGED
@@ -48,14 +48,14 @@ export const ECRunner = {
48
48
  // --- ethereum sepolia ---
49
49
  'ecld-pynithy-ethereum-sepolia': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
50
50
  'ecld-nodenithy-ethereum-sepolia': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
51
- // --- litvm liteforge testnet (shares sepolia contracts; distinct chain/RPC) ---
52
- 'ecld-pynithy-litvm-testnet': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
53
- 'ecld-nodenithy-litvm-testnet': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
51
+ // --- litvm liteforge testnet: the PoX is the EthernityImplementationV2 1.1 of 2026-10-11; the image registry is the ECImageRegistryV3 of 2026-10-12 ---
52
+ 'ecld-pynithy-litvm-testnet': ['0xE35991de1f2A435e29E70406FA2781b1AffCb168', '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
53
+ 'ecld-nodenithy-litvm-testnet': ['0xE35991de1f2A435e29E70406FA2781b1AffCb168', '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
54
54
  // --- the -unsafe networks: the contracts of the network each is named after ---
55
55
  'etny-pynithy-testnet-unsafe': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1', 'https://bloxberg.ethernity.cloud', 8995],
56
56
  'etny-nodenithy-testnet-unsafe': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1', 'https://bloxberg.ethernity.cloud', 8995],
57
- 'ecld-pynithy-litvm-testnet-unsafe': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
58
- 'ecld-nodenithy-litvm-testnet-unsafe': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441]
57
+ 'ecld-pynithy-litvm-testnet-unsafe': ['0xE35991de1f2A435e29E70406FA2781b1AffCb168', '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
58
+ 'ecld-nodenithy-litvm-testnet-unsafe': ['0xE35991de1f2A435e29E70406FA2781b1AffCb168', '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441]
59
59
  };
60
60
 
61
61
  // ethernity-cas ValidatorRegistry per template, baked into the enclave so it
package/pynithy/ipfs.mjs CHANGED
@@ -1,6 +1,10 @@
1
1
  // ipfs.mjs
2
2
  //
3
- // IPFS client for the Ethernity Cloud JS SDK.
3
+ // IPFS client of the publish (nodenithy/run.js, pynithy/run.js): adds, hashes
4
+ // and reads through the IPFS API named with --host, the publish's own Kubo
5
+ // (http://127.0.0.1:<port>) or an IPFS API of the dApp's own; --token, or
6
+ // IPFS_TOKEN in the environment when the flag is absent, is the Authorization
7
+ // header value that API takes, when it takes one.
4
8
  //
5
9
  // The UPLOAD path is a faithful port of the Python SDK's IPFSClient
6
10
  // (ethernity_cloud_sdk_py/commands/pynithy/ipfs_client.py): it posts directly
@@ -12,7 +16,7 @@
12
16
  // the Python pipeline step-for-step so both SDKs upload identically (e.g. for
13
17
  // the certex / public-key extraction flow).
14
18
  //
15
- // The DOWNLOAD path keeps the existing kubo-rpc-client based helpers.
19
+ // The DOWNLOAD path uses kubo-rpc-client, with the same Authorization header.
16
20
  import http from 'http';
17
21
  import https from 'https';
18
22
  import { URL } from 'url';
@@ -31,29 +35,33 @@ const getRetryDelay = (retryCount, baseDelay = 1) => baseDelay * 2 ** retryCount
31
35
  const RETRY_COUNT = 10; // mirrors Python ipfs_client.RETRY_COUNT
32
36
 
33
37
  // Endpoint + auth, mirroring the Python IPFSClient constructor.
34
- // apiUrl : e.g. "https://ipfs.ethernity.cloud"
35
- // addUrl : `${apiUrl}/api/v0/add`
38
+ // apiUrl : the API's base URL, e.g. "http://127.0.0.1:5001"; adds go to
39
+ // `${apiUrl}/api/v0/add`
40
+ // authHeader : the Authorization header value (--token), or null
36
41
  let apiUrl = null;
37
- let authHeader = null; // Authorization header value, or null
42
+ let authHeader = null;
38
43
 
39
44
  // kubo-rpc-client instance, used only for the download helpers below.
40
45
  let ipfs = null;
41
46
 
42
47
  const initialize = (host, protocol, port, token) => {
48
+ authHeader = token && token !== '' ? token : null;
43
49
  // Normalise the endpoint to a base URL string, matching how the Python client
44
50
  // treats `ipfs_endpoint` (it just prepends it to `/api/v0/add`).
51
+ let opts;
45
52
  if (host && host.search('http') !== -1) {
46
- apiUrl = host.replace(/\/+$/, '');
47
- ipfs = create(host);
53
+ // The API's base URL: a trailing /api/v0 is dropped, since addUrl appends it.
54
+ apiUrl = host.replace(/\/+$/, '').replace(/\/api\/v0$/, '');
55
+ opts = { url: apiUrl };
48
56
  } else {
49
57
  const proto = protocol || 'http';
50
58
  const p = port ? `:${port}` : '';
51
59
  apiUrl = `${proto}://${host}${p}`.replace(/\/+$/, '');
52
- const opts = { host, protocol: proto, port };
53
- if (token) opts.headers = { authorization: token };
54
- ipfs = create(opts);
60
+ opts = { host, protocol: proto, port };
55
61
  }
56
- authHeader = token && token !== '' ? token : null;
62
+ // The reads send the same Authorization header as the adds.
63
+ if (authHeader) opts.headers = { authorization: authHeader };
64
+ ipfs = create(opts);
57
65
  };
58
66
 
59
67
  // ---------------------------------------------------------------------------
@@ -336,8 +344,48 @@ const hashForIPFS = async (p) => {
336
344
  return null;
337
345
  };
338
346
 
347
+ // Pin `cid` on the API (`pin/add`), which fetches the blocks it does not hold
348
+ // from the peers that provide them and answers once the pin is complete.
349
+ // Resolves true when the API names the CID among its pins.
350
+ const pinOnIPFS = (cid, timeoutMs = 3600000) =>
351
+ new Promise((resolve, reject) => {
352
+ const url = new URL(`${apiUrl}/api/v0/pin/add?arg=${encodeURIComponent(cid)}&recursive=true`);
353
+ const mod = url.protocol === 'https:' ? https : http;
354
+ const headers = { 'Content-Length': '0' };
355
+ if (authHeader) headers.Authorization = authHeader;
356
+ const req = mod.request(
357
+ {
358
+ method: 'POST',
359
+ hostname: url.hostname,
360
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
361
+ path: `${url.pathname}${url.search}`,
362
+ headers,
363
+ timeout: timeoutMs
364
+ },
365
+ (res) => {
366
+ let text = '';
367
+ res.setEncoding('utf8');
368
+ res.on('data', (chunk) => {
369
+ text += chunk;
370
+ });
371
+ res.on('end', () => {
372
+ if (res.statusCode !== 200) {
373
+ console.log(`Failed to pin ${cid} on ${apiUrl}: status ${res.statusCode}`);
374
+ console.log(text.slice(0, 300));
375
+ resolve(false);
376
+ return;
377
+ }
378
+ resolve(text.includes(cid));
379
+ });
380
+ }
381
+ );
382
+ req.on('timeout', () => req.destroy(new Error(`pin/add of ${cid} took longer than ${timeoutMs} ms`)));
383
+ req.on('error', reject);
384
+ req.end();
385
+ });
386
+
339
387
  // ---------------------------------------------------------------------------
340
- // Download helpers (kubo-rpc-client based; unchanged behaviour)
388
+ // Download helpers (kubo-rpc-client, with the Authorization header of initialize())
341
389
  // ---------------------------------------------------------------------------
342
390
 
343
391
  const getFromIPFS = async (hhash, filePath, maxRetries = process.env.REACT_APP_IPFS_RETRIES || 5) => {
@@ -412,6 +460,7 @@ export {
412
460
  initialize,
413
461
  upload,
414
462
  hashForIPFS,
463
+ pinOnIPFS,
415
464
  uploadFileToIPFS,
416
465
  uploadFolderToIPFS,
417
466
  getFromIPFS,
@@ -424,12 +473,12 @@ export {
424
473
  // ---------------------------------------------------------------------------
425
474
 
426
475
  program
427
- .option('--host <host>', 'IPFS host')
476
+ .requiredOption('--host <host>', "IPFS API URL: the publish's own Kubo (http://127.0.0.1:<port>) or an IPFS API of the dApp's own")
428
477
  .option('--hhash <hhash>', 'IPFS hash')
429
478
  .option('--filePath <path>', 'Path to the file')
430
479
  .option('--folderPath <path>', 'Path to the folder')
431
- .option('--action <action>', 'Action to perform (upload, hash, download)')
432
- .option('--token <token>', 'IPFS authorization token')
480
+ .option('--action <action>', 'Action to perform (upload, hash, pin, download)')
481
+ .option('--token <token>', 'Authorization header value for the API (IPFS_TOKEN in the environment when absent)')
433
482
  .option('--output <path>', 'Output path for download');
434
483
 
435
484
  const isMain = process.argv[1] && process.argv[1].endsWith('ipfs.mjs');
@@ -439,8 +488,7 @@ if (isMain) {
439
488
  const options = program.opts();
440
489
 
441
490
  const main = async () => {
442
- const host = options.host || 'localhost';
443
- initialize(host, undefined, undefined, options.token || '');
491
+ initialize(options.host, undefined, undefined, options.token || process.env.IPFS_TOKEN || '');
444
492
 
445
493
  if (options.action === 'upload') {
446
494
  if (options.filePath) {
@@ -469,6 +517,14 @@ if (isMain) {
469
517
  const hhash = await hashForIPFS(target);
470
518
  console.log(`${hhash}`);
471
519
  process.exit(hhash ? 0 : 1);
520
+ } else if (options.action === 'pin') {
521
+ if (!options.hhash) {
522
+ console.error('Please provide --hhash, the CID to pin.');
523
+ process.exit(1);
524
+ }
525
+ const pinned = await pinOnIPFS(options.hhash);
526
+ if (pinned) console.log(`${options.hhash}`);
527
+ process.exit(pinned ? 0 : 1);
472
528
  } else if (options.action === 'download') {
473
529
  if (options.filePath) {
474
530
  await getFromIPFS(options.hhash, options.filePath);
@@ -480,7 +536,7 @@ if (isMain) {
480
536
  console.error('Please provide a filePath or folderPath for download.');
481
537
  }
482
538
  } else {
483
- console.error('Please provide a valid action (upload, hash, download).');
539
+ console.error('Please provide a valid action (upload, hash, pin, download).');
484
540
  process.exit(1);
485
541
  }
486
542
  };
@@ -3,7 +3,10 @@ version: '3.2'
3
3
  services:
4
4
  minio:
5
5
  container_name: etny-swift-stream
6
- image: quay.io/minio/minio:RELEASE.2023-03-13T19-46-17Z
6
+ # quay.io/minio/minio:RELEASE.2023-03-13T19-46-17Z, mirrored on 2026-10-13
7
+ # (image id c69ff79d5831): quay.io answers 401 to an anonymous pull of
8
+ # that tag, and the mirror is pulled without credentials.
9
+ image: registry.ethernity.cloud:443/debuggingdelight/ethernity-cloud-sdk-registry/minio/minio:RELEASE.2023-03-13T19-46-17Z
7
10
  command: server /data/
8
11
  expose:
9
12
  - "9000"
@@ -3,7 +3,10 @@ version: '3.2'
3
3
  services:
4
4
  minio:
5
5
  container_name: etny-swift-stream
6
- image: quay.io/minio/minio:RELEASE.2023-03-13T19-46-17Z
6
+ # quay.io/minio/minio:RELEASE.2023-03-13T19-46-17Z, mirrored on 2026-10-13
7
+ # (image id c69ff79d5831): quay.io answers 401 to an anonymous pull of
8
+ # that tag, and the mirror is pulled without credentials.
9
+ image: registry.ethernity.cloud:443/debuggingdelight/ethernity-cloud-sdk-registry/minio/minio:RELEASE.2023-03-13T19-46-17Z
7
10
  command: server /data/
8
11
  expose:
9
12
  - "9000"
@@ -39,7 +39,7 @@ function setVars(network = "") {
39
39
  }
40
40
  } else if (BLOCKCHAIN_NETWORK.includes("LitVM")) {
41
41
  NETWORK_RPC = "https://liteforge.rpc.caldera.xyz/infra-partner-http";
42
- IMAGE_REGISTRY_ADDRESS = "0x55e0ad455Be85162b71a790f00Fc305680E3CE53";
42
+ IMAGE_REGISTRY_ADDRESS = "0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D";
43
43
  CHAIN_ID = 4441;
44
44
  }
45
45
  }
package/pynithy/run.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const fs = require('fs');
2
- const { execSync } = require('child_process');
2
+ const { execSync, execFileSync } = require('child_process');
3
3
  require('dotenv').config();
4
4
  const readline = require('readline');
5
5
  const forge = require('node-forge');
@@ -8,7 +8,7 @@ const https = require('https');
8
8
  const casConfig = require('../cas/config.js');
9
9
  const casResolver = require('../cas/resolver.js');
10
10
  const sessionRegistry = require('../cas/session_registry.js');
11
- const { LocalKubo, ownEndpoint } = require('../localKubo.js');
11
+ const { LocalKubo, ownEndpoint, PUBLIC_IPFS_HOST } = require('../localKubo.js');
12
12
 
13
13
  if (!fs.existsSync('.env')) {
14
14
  console.error("Error: .env file not found");
@@ -122,13 +122,17 @@ const main = async () => {
122
122
  }
123
123
 
124
124
  // Where the image goes. An IPFS API of the application's own
125
- // (IPFS_ENDPOINT, when it is not the public one) takes precedence.
126
- // Otherwise the publish runs its own Kubo: the public write API of
127
- // ipfs.ethernity.cloud is closing, and the image is held by this Kubo,
128
- // peered with the bootnode, until its certificate is on chain.
125
+ // (IPFS_ENDPOINT, with IPFS_TOKEN as its Authorization header when set)
126
+ // takes precedence. Otherwise the publish runs its own Kubo: the public
127
+ // API of ipfs.ethernity.cloud takes no adds or pins without a writer
128
+ // token, and the image is held by this Kubo, peered with the bootnode,
129
+ // until its certificate is on chain.
129
130
  let localKubo = null;
130
131
  let ipfsApi = process.env.IPFS_ENDPOINT || '';
131
- if (!ownEndpoint(ipfsApi)) {
132
+ let ipfsToken = '';
133
+ if (ownEndpoint(ipfsApi)) {
134
+ ipfsToken = process.env.IPFS_TOKEN || '';
135
+ } else {
132
136
  localKubo = new LocalKubo(process.env.PROJECT_NAME);
133
137
  try {
134
138
  await localKubo.start();
@@ -140,23 +144,28 @@ const main = async () => {
140
144
  console.log(`\t✔ IPFS node for this publish: ${ipfsApi}`);
141
145
  process.on('exit', () => localKubo.release());
142
146
  }
147
+ // node ../ipfs.mjs against ipfsApi, with the token as IPFS_TOKEN in its
148
+ // environment: the command line is in a failure's message and in the
149
+ // process list. A non-zero exit ends the publish.
150
+ const ipfsEnv = { ...process.env, IPFS_TOKEN: ipfsToken };
151
+ const ipfsCli = (args, stdio) => {
152
+ try {
153
+ return execFileSync(process.execPath, ['../ipfs.mjs', '--host', ipfsApi, ...args], { stdio, env: ipfsEnv });
154
+ } catch (e) {
155
+ console.error(`Error: node ../ipfs.mjs ${args.join(' ')} failed (${e.status != null ? `exit ${e.status}` : (e.signal || e.code)}).`);
156
+ process.exit(1);
157
+ }
158
+ };
143
159
 
144
160
  // The CIDs uploadToIpfs gives, from the same add calls with only-hash: the
145
161
  // IPFS node stores and announces nothing. Set by hashForIpfs, after which
146
162
  // uploadToIpfs refuses an upload that gives other CIDs.
147
163
  let expectedCids = null;
148
164
  const hashForIpfs = () => {
149
- const hashOf = (target) => {
150
- try {
151
- const out = execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action hash ${target}`, { stdio: ['ignore', 'pipe', 'inherit'] });
152
- return out.toString().trim().split('\n').pop().trim();
153
- } catch (e) {
154
- console.error(`Error: could not hash ${target} for IPFS: ${e.message}`);
155
- process.exit(1);
156
- }
157
- };
158
- process.env.IPFS_DOCKER_COMPOSE_HASH = hashOf('--filePath docker-compose-final.yml');
159
- process.env.IPFS_HASH = hashOf(`--folderPath ${registryPath}`);
165
+ const hashOf = (target) => ipfsCli(['--action', 'hash', ...target], ['ignore', 'pipe', 'inherit'])
166
+ .toString().trim().split('\n').pop().trim();
167
+ process.env.IPFS_DOCKER_COMPOSE_HASH = hashOf(['--filePath', 'docker-compose-final.yml']);
168
+ process.env.IPFS_HASH = hashOf(['--folderPath', registryPath]);
160
169
  console.log("IPFS_DOCKER_COMPOSE_HASH (before upload): ", process.env.IPFS_DOCKER_COMPOSE_HASH);
161
170
  console.log("IPFS_HASH (before upload): ", process.env.IPFS_HASH);
162
171
  expectedCids = { image: process.env.IPFS_HASH, compose: process.env.IPFS_DOCKER_COMPOSE_HASH };
@@ -169,7 +178,7 @@ const main = async () => {
169
178
  if (fs.existsSync(file)) fs.unlinkSync(file);
170
179
  }
171
180
  console.log('Upload docker-compose-final.yml to IPFS');
172
- execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action upload --filePath docker-compose-final.yml`, { stdio: "inherit" });
181
+ ipfsCli(['--action', 'upload', '--filePath', 'docker-compose-final.yml'], 'inherit');
173
182
  if (!fs.existsSync('IPFS_DOCKER_COMPOSE_HASH.ipfs')) {
174
183
  console.error("Error: Could not upload docker-compose-final.yml to IPFS, please try again!");
175
184
  process.exit(1);
@@ -178,7 +187,7 @@ const main = async () => {
178
187
  console.log("IPFS_DOCKER_COMPOSE_HASH: ", process.env.IPFS_DOCKER_COMPOSE_HASH);
179
188
  writeEnv('IPFS_DOCKER_COMPOSE_HASH', process.env.IPFS_DOCKER_COMPOSE_HASH);
180
189
  console.log('Upload docker registry to IPFS');
181
- execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action upload --folderPath ${registryPath}`, { stdio: "inherit" });
190
+ ipfsCli(['--action', 'upload', '--folderPath', registryPath], 'inherit');
182
191
  if (!fs.existsSync('IPFS_HASH.ipfs')) {
183
192
  console.error("Error: Could not upload registry to IPFS, please try again!");
184
193
  process.exit(1);
@@ -323,7 +332,7 @@ const main = async () => {
323
332
  const registered = await sessionRegistry.register(
324
333
  casChain.rpcUrl, casChain.chainId, casConfig.SESSION_REGISTRY[templateName],
325
334
  process.env.PRIVATE_KEY, fs.readFileSync('etny-securelock-test.yaml'),
326
- ipfsApi, '');
335
+ ipfsApi, '', ipfsToken);
327
336
  if (localKubo) await localKubo.provide(registered.cid);
328
337
  if (registered.registered) {
329
338
  console.log(`\t✔ Session ${registered.name} registered on-chain (body ${registered.cid})`);
@@ -588,6 +597,29 @@ const main = async () => {
588
597
  if (registryV2 && localKubo) {
589
598
  await localKubo.provide(process.env.IPFS_HASH);
590
599
  await localKubo.provide(process.env.IPFS_DOCKER_COMPOSE_HASH);
600
+ } else if (!registryV2 && localKubo) {
601
+ // On a V1 registry nothing is recorded before the certificate, so the
602
+ // bootnode's mirror pins nothing from this Kubo, and the certificate
603
+ // extraction service and the nodes, which fetch from the bootnode,
604
+ // would wait on an image it never receives: the compose and the image
605
+ // are pinned on the public API, which fetches them from this Kubo over
606
+ // the connection it holds. The pin takes a token (IPFS_TOKEN); without
607
+ // one the publish stops here, before anything is on chain.
608
+ const publicToken = process.env.IPFS_TOKEN || '';
609
+ if (!publicToken) {
610
+ console.error("Error: this network's image registry records an image only with its certificate, so the image must be pinned on the public IPFS API before the certificate is extracted, and the public API takes a pin only with a token.");
611
+ console.error(` Set IPFS_TOKEN to a writer token of ${PUBLIC_IPFS_HOST} and publish again.`);
612
+ process.exit(1);
613
+ }
614
+ for (const cid of [process.env.IPFS_DOCKER_COMPOSE_HASH, process.env.IPFS_HASH]) {
615
+ console.log(`Pinning ${cid} on the public IPFS API`);
616
+ try {
617
+ execFileSync(process.execPath, ['../ipfs.mjs', '--host', `https://${PUBLIC_IPFS_HOST}`, '--action', 'pin', '--hhash', cid], { stdio: 'inherit', env: { ...process.env, IPFS_TOKEN: publicToken } });
618
+ } catch (e) {
619
+ console.error(`Error: the public IPFS API did not pin ${cid} (${e.status != null ? `exit ${e.status}` : (e.signal || e.code)}).`);
620
+ process.exit(1);
621
+ }
622
+ }
591
623
  }
592
624
  if (casTestnet) await registerSessionOnChain();
593
625
 
@@ -699,7 +731,7 @@ const main = async () => {
699
731
  fs.unlinkSync('IPFS_HASH.ipfs');
700
732
  }
701
733
  console.log('Upload docker registry to IPFS');
702
- execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action upload --folderPath ${registryPath}`, { stdio: "inherit" });
734
+ ipfsCli(['--action', 'upload', '--folderPath', registryPath], 'inherit');
703
735
  if (!fs.existsSync(`./IPFS_HASH.ipfs`)) {
704
736
  console.error("Error: Could not upload registry to IPFS, please try again!");
705
737
  process.exit(1);
@@ -5,7 +5,8 @@ import { ECStatus } from "@ethernity-cloud/runner/enums";
5
5
  // Filled in from the project's .env by src/preStart.mjs on `npm start`: the
6
6
  // securelock this dApp is published as, the trustedzone it was built against,
7
7
  // the network's token address and chain id, the -unsafe network when the dApp
8
- // runs without a CAS (null otherwise), and the IPFS endpoint.
8
+ // runs without a CAS (null otherwise), and the IPFS API of the dApp's own ('' when
9
+ // the task's artefacts go through the bootnode's payload intake).
9
10
  const PROJECT_NAME = "";
10
11
  const TRUSTED_ZONE_IMAGE = "";
11
12
  const NETWORK_ADDRESS = "";
@@ -23,7 +24,10 @@ function App() {
23
24
  if (UNSAFE_NETWORK) {
24
25
  await runner.setNetwork(...UNSAFE_NETWORK);
25
26
  }
26
- runner.initializeStorage(IPFS_ENDPOINT);
27
+ // An IPFS API of the dApp's own (IPFS_ENDPOINT) carries the task's
28
+ // artefacts; without one they go through the bootnode's payload intake.
29
+ if (IPFS_ENDPOINT) runner.initializeStorage(IPFS_ENDPOINT);
30
+ else runner.initializePublicIntake();
27
31
 
28
32
  // Events are named by task status; detail is { message, status, progress }.
29
33
  runner.addEventListener(ECStatus.DEFAULT, (e) => {
@@ -3,7 +3,10 @@
3
3
  // .env, the way ecld-run reads the same values. PROJECT_NAME and
4
4
  // TRUSTED_ZONE_IMAGE take their -unsafe form on an -unsafe network; the
5
5
  // network's token address, chain id and -unsafe (network, type) come from the
6
- // runner's own enums through the SDK's network table.
6
+ // runner's own enums through the SDK's network table; IPFS_ENDPOINT is the
7
+ // IPFS API of the dApp's own, or '' (no setting, or the public API, which
8
+ // takes no writes without a token), on which the dApp delivers its task
9
+ // artefacts through the bootnode's payload intake.
7
10
  import fs from 'fs';
8
11
  import { createRequire } from 'module';
9
12
  import dotenv from 'dotenv';
@@ -13,6 +16,7 @@ dotenv.config();
13
16
  const require = createRequire(import.meta.url);
14
17
  const casConfig = require('@ethernity-cloud/sdk-js/cas/config.js');
15
18
  const { networkFor } = require('@ethernity-cloud/sdk-js/network.js');
19
+ const { dappEndpoint } = require('@ethernity-cloud/sdk-js/localKubo.js');
16
20
 
17
21
  const { PROJECT_NAME, TRUSTED_ZONE_IMAGE, BLOCKCHAIN_NETWORK, IPFS_ENDPOINT } = process.env;
18
22
  if (!PROJECT_NAME || !BLOCKCHAIN_NETWORK) {
@@ -31,7 +35,7 @@ const constants = {
31
35
  NETWORK_ADDRESS: network.address,
32
36
  CHAIN_ID: network.chainId || null,
33
37
  UNSAFE_NETWORK: network.unsafe || null,
34
- IPFS_ENDPOINT: IPFS_ENDPOINT || 'https://ipfs.ethernity.cloud',
38
+ IPFS_ENDPOINT: dappEndpoint(IPFS_ENDPOINT),
35
39
  };
36
40
 
37
41
  const filePath = 'src/ec_helloworld_example.js';