@ethernity-cloud/sdk-js 1.7.9 → 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/README.md CHANGED
@@ -56,7 +56,7 @@ The sdk has been tested on the following operating systems:
56
56
  - Mainnet - to be provided during the following updates
57
57
  - LitVM:
58
58
  - LiteForge - CAS-attested the same way, by LitVM's own ethernity-cas
59
- validator set (SessionRegistry `0x8ad24b3F...`, ValidatorRegistry
59
+ validator set (SessionRegistry `0xfA70B5Df...`, ValidatorRegistry
60
60
  `0xbE3759f3...`).
61
61
  - LiteForge Unsafe - the unsafe network beside LiteForge (see below).
62
62
  - Polyhon:
@@ -120,11 +120,14 @@ sets the enclave heap (default `1024M`).
120
120
  unless `REWARD_ADDRESS` names another one; the image's owner can change it
121
121
  later with the registry's `changeImageRewardAddress`.
122
122
 
123
- How the image reaches the network: unless `.env` names an IPFS API of your
124
- own (`IPFS_ENDPOINT` other than the public `ipfs.ethernity.cloud`), the
125
- publish runs its own IPFS node in docker (`ipfs/kubo`), adds the image and
126
- its compose there and peers it with the network's bootnode, which pins
127
- every registered image. On a network whose image registry records images
123
+ How the image reaches the network: leave `IPFS_ENDPOINT` in `.env` empty
124
+ unless it names an IPFS API of your own (`IPFS_TOKEN` is the Authorization
125
+ header value that API takes, when it takes one); the public API
126
+ `ipfs.ethernity.cloud` takes no adds or pins without a writer token. With
127
+ no API of your own the publish runs its own IPFS node in docker
128
+ (`ipfs/kubo`), adds the image and its compose there and peers it with the
129
+ network's bootnode, which pins every registered image. On a network whose
130
+ image registry records images
128
131
  before their certificate (the bloxberg testnet), the publish registers the
129
132
  image from your wallet first (`registerImage`, with your node's address),
130
133
  has the certificate extracted (on your SGX host, or by
@@ -160,13 +163,22 @@ sets the enclave heap (default `1024M`).
160
163
  npx ecld-run --unsafe 'hello("World")'
161
164
  ```
162
165
  This is the network-side sibling of `ecld-test`: instead of executing locally
163
- it drives the runner end to end — encrypt → IPFS → on-chain request → wait for
164
- a node → download and decrypt the result. It **costs gas and needs a funded
165
- key**. Network and enclaves come from `.env` (`BLOCKCHAIN_NETWORK`,
166
- `PROJECT_NAME`, `TRUSTED_ZONE_IMAGE`), overridable with
166
+ it drives the runner end to end — encrypt → on-chain request → payload intake
167
+ (or the IPFS API named with `--ipfs <url>`) → wait for a node → download and
168
+ decrypt the result. It **costs gas and needs a funded key**. Network and
169
+ enclaves come from `.env` (`BLOCKCHAIN_NETWORK`, `PROJECT_NAME`,
170
+ `TRUSTED_ZONE_IMAGE`), overridable with
167
171
  `--network`/`--securelock`/`--trustedzone`; the signing key is `PRIVATE_KEY`
168
172
  (or `ECLD_PRIVATE_KEY`) — a funded `0x` key that pays for the order. Resources
169
173
  are tunable with `--task-price`/`--cpu`/`--memory`/`--storage`/`--bandwidth`/`--duration`/`--validators`.
174
+ `--ipfs intake` (the default) delivers the payload to the bootnode's payload
175
+ intake once the request is on chain; `--ipfs <url>` writes and reads through
176
+ that IPFS RPC API instead, with `ECLD_IPFS_TOKEN` (or `IPFS_TOKEN`) as its
177
+ Authorization header, and a URL naming the public API is refused without one.
178
+ The intake serves the networks the bootnode's agent runs, today the bloxberg
179
+ testnet and LitVM LiteForge, each with its unsafe twin: a dApp on any other
180
+ network (Bloxberg mainnet, Polygon, IoTeX, Sepolia) needs an IPFS API of its
181
+ own (`IPFS_ENDPOINT` for the Hello World template, `--ipfs <url>` for `ecld-run`).
170
182
  Exit code 0 on a `SUCCESS` task result, 1 otherwise. `--unsafe` runs the
171
183
  network's unsafe variant; on an unsafe network the securelock and
172
184
  trustedzone default to their `-unsafe` names.
@@ -214,13 +226,17 @@ function hello(msg='World') {
214
226
 
215
227
  module.exports = { hello };
216
228
  ```
217
- From your frontend application, using the ethernity cloud runner library (`@ethernity-cloud/runner`, installed by `ecld-init` at the version this SDK depends on), you will be calling the function as seen in the below example, where we pass `hello("World");` to be executed on the backend which will run in the Blockchain. `npm start` runs `src/preStart.mjs` first, which fills the constants at the top from the project's `.env`: the securelock name the dApp is published as, the trustedzone it was built against, the network's token address and chain id, the `-unsafe` network when the dApp runs without a CAS, and the IPFS endpoint.
229
+ From your frontend application, using the ethernity cloud runner library (`@ethernity-cloud/runner`, installed by `ecld-init` at the version this SDK depends on), you will be calling the function as seen in the below example, where we pass `hello("World");` to be executed on the backend which will run in the Blockchain. `npm start` runs `src/preStart.mjs` first, which fills the constants at the top from the project's `.env`: the securelock name the dApp is published as, the trustedzone it was built against, the network's token address and chain id, the `-unsafe` network when the dApp runs without a CAS, and the IPFS API of the dApp's own (empty when the task's artefacts go through the bootnode's payload intake).
218
230
  ```js
219
231
  import './App.css';
220
232
  import EthernityCloudRunner from "@ethernity-cloud/runner";
221
233
  import { ECStatus } from "@ethernity-cloud/runner/enums";
222
234
 
223
- // Filled in from the project's .env by src/preStart.mjs on `npm start`.
235
+ // Filled in from the project's .env by src/preStart.mjs on `npm start`: the
236
+ // securelock this dApp is published as, the trustedzone it was built against,
237
+ // the network's token address and chain id, the -unsafe network when the dApp
238
+ // runs without a CAS (null otherwise), and the IPFS API of the dApp's own ('' when
239
+ // the task's artefacts go through the bootnode's payload intake).
224
240
  const PROJECT_NAME = "";
225
241
  const TRUSTED_ZONE_IMAGE = "";
226
242
  const NETWORK_ADDRESS = "";
@@ -238,7 +254,10 @@ function App() {
238
254
  if (UNSAFE_NETWORK) {
239
255
  await runner.setNetwork(...UNSAFE_NETWORK);
240
256
  }
241
- runner.initializeStorage(IPFS_ENDPOINT);
257
+ // An IPFS API of the dApp's own (IPFS_ENDPOINT) carries the task's
258
+ // artefacts; without one they go through the bootnode's payload intake.
259
+ if (IPFS_ENDPOINT) runner.initializeStorage(IPFS_ENDPOINT);
260
+ else runner.initializePublicIntake();
242
261
 
243
262
  // Events are named by task status; detail is { message, status, progress }.
244
263
  runner.addEventListener(ECStatus.DEFAULT, (e) => {
@@ -250,6 +269,7 @@ function App() {
250
269
  runner.addEventListener(ECStatus.SUCCESS, async () => {
251
270
  const result = await runner.getResult();
252
271
  console.log(`Task Result: ${result}`);
272
+ // display the result in page below the buttons
253
273
  const line = document.createElement("p");
254
274
  line.textContent = `Task Result: ${result}`;
255
275
  document.body.appendChild(line);
package/cas/config.js CHANGED
@@ -23,8 +23,8 @@ const VALIDATOR_REGISTRY = {
23
23
  const SESSION_REGISTRY = {
24
24
  'etny-pynithy-testnet': '0x444429681fCb26470Cb12B5436E48402cF7478A3',
25
25
  'etny-nodenithy-testnet': '0x444429681fCb26470Cb12B5436E48402cF7478A3',
26
- 'ecld-pynithy-litvm-testnet': '0x8ad24b3F406A41a0F8D3440021792EB203957F43',
27
- 'ecld-nodenithy-litvm-testnet': '0x8ad24b3F406A41a0F8D3440021792EB203957F43',
26
+ 'ecld-pynithy-litvm-testnet': '0xfA70B5Df7C773A21164cAc5413704117e4dEaB76',
27
+ 'ecld-nodenithy-litvm-testnet': '0xfA70B5Df7C773A21164cAc5413704117e4dEaB76',
28
28
  };
29
29
 
30
30
  // The chain the registries above live on.
@@ -74,11 +74,13 @@ function sessionHash(body) {
74
74
  return '0x' + crypto.createHash('sha256').update(body).digest('hex');
75
75
  }
76
76
 
77
- // Pin the exact bytes as CIDv1/raw/sha2-256 and return the CID.
78
- async function pinBody(ipfsApiUrl, body) {
77
+ // Pin the exact bytes as CIDv1/raw/sha2-256 and return the CID. `token` is
78
+ // the API's Authorization header value (IPFS_TOKEN), sent when set.
79
+ async function pinBody(ipfsApiUrl, body, token = '') {
79
80
  const form = new FormData();
80
81
  form.append('file', new Blob([body]), 'session');
81
- const r = await axios.post(`${ipfsApiUrl}/api/v0/add?cid-version=1&raw-leaves=true&pin=true`, form, { timeout: 60000 });
82
+ const headers = token ? { Authorization: token } : {};
83
+ const r = await axios.post(`${ipfsApiUrl}/api/v0/add?cid-version=1&raw-leaves=true&pin=true`, form, { headers, timeout: 60000 });
82
84
  return r.data.Hash;
83
85
  }
84
86
 
@@ -120,16 +122,17 @@ async function send(reg, chainId, refused, method, ...args) {
120
122
  if (rcpt.status !== 1) throw new Error(`SessionRegistry.${method} reverted for ${refused} (tx ${tx.hash})`);
121
123
  }
122
124
 
123
- // Pin `body` and register it under its own `name:`. Returns
125
+ // Pin `body` through `ipfsApiUrl` (with `ipfsToken` as its Authorization
126
+ // header when set) and register it under its own `name:`. Returns
124
127
  // {name, hash, cid, registered}: `registered` is false when the chain already
125
128
  // held these exact bytes as the name's latest version, registered by this
126
129
  // wallet.
127
- async function register(rpcUrl, chainId, registryAddress, key, body, ipfsApiUrl, imageCid = '') {
130
+ async function register(rpcUrl, chainId, registryAddress, key, body, ipfsApiUrl, imageCid = '', ipfsToken = '') {
128
131
  const { name, rules } = parseNameAndRules(body.toString('utf8'));
129
132
  if (!name) throw new Error('the session body has no `name:`');
130
133
  const hash = sessionHash(body);
131
134
  const reg = contract(rpcUrl, registryAddress, key);
132
- const cid = await pinBody(ipfsApiUrl, body);
135
+ const cid = await pinBody(ipfsApiUrl, body, ipfsToken);
133
136
  const latest = await latestRecord(reg, name);
134
137
  if (latest && latest.hash === hash && latest.creator.toLowerCase() === (await reg.signer.getAddress()).toLowerCase()) {
135
138
  return { name, hash, cid, registered: false };
package/info.js CHANGED
@@ -51,8 +51,8 @@ const NETWORKS = {
51
51
  },
52
52
  LITVM_LITEFORGE: {
53
53
  type: 'testnet', chainId: 4441, rpc: 'https://liteforge.rpc.caldera.xyz/infra-partner-http',
54
- protocol: '0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2',
55
- imageRegistry: '0x55e0ad455Be85162b71a790f00Fc305680E3CE53',
54
+ protocol: '0xE35991de1f2A435e29E70406FA2781b1AffCb168',
55
+ imageRegistry: '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D',
56
56
  esr: '0x709052Fe77Af543d3d9FE2Ac06a15c635c8D4Be5',
57
57
  },
58
58
  };
package/init.js CHANGED
@@ -4,10 +4,8 @@ const { spawn, execSync } = require("child_process");
4
4
  const path = require("path");
5
5
  const casConfig = require("./cas/config.js");
6
6
 
7
- const rl = readline.createInterface({
8
- input: process.stdin,
9
- output: process.stdout,
10
- });
7
+ // The prompt, opened by main.
8
+ let rl = null;
11
9
 
12
10
  const writeEnv = (key, value) => {
13
11
  const envFile = ".env";
@@ -36,6 +34,23 @@ const writeEnv = (key, value) => {
36
34
  // name does not contain "SECURELOCK".
37
35
  const PROJECT_NAME_RULE = /^(?!.*SECURELOCK)[0-9A-Za-z._-]{1,121}$/;
38
36
 
37
+ // Where the publish adds the image and the dApp delivers its task artefacts.
38
+ // The first choice has the publish run its own IPFS node and the dApp use the
39
+ // bootnode's payload intake (IPFS_ENDPOINT empty); the second names an IPFS
40
+ // (Kubo) RPC API of the dApp's own, with the Authorization header value it
41
+ // takes. The public API, ipfs.ethernity.cloud, takes no adds or pins without
42
+ // a writer token.
43
+ const IPFS_OPTIONS = [
44
+ "the publish's own IPFS node in docker (default)",
45
+ "an IPFS (Kubo) RPC API of your own (URL, optional Authorization token)",
46
+ ];
47
+
48
+ // The .env values (IPFS_ENDPOINT, IPFS_TOKEN) of an IPFS_OPTIONS choice.
49
+ const ipfsSettings = (choice, url, token) =>
50
+ choice === IPFS_OPTIONS[1]
51
+ ? { IPFS_ENDPOINT: String(url || "").trim(), IPFS_TOKEN: String(token || "").trim() }
52
+ : { IPFS_ENDPOINT: "", IPFS_TOKEN: "" };
53
+
39
54
  const getProjectName = () => {
40
55
  return new Promise((resolve) => {
41
56
  const askProjectName = () => {
@@ -159,6 +174,10 @@ const printIntro = () => {
159
174
  };
160
175
 
161
176
  const main = async () => {
177
+ rl = readline.createInterface({
178
+ input: process.stdin,
179
+ output: process.stdout,
180
+ });
162
181
  printIntro();
163
182
  const projectName = await getProjectName();
164
183
  console.log();
@@ -256,30 +275,28 @@ const main = async () => {
256
275
  // });
257
276
 
258
277
  console.log();
259
- const ipfsServiceOptions = ["Ethernity (best effort)", "Custom IPFS"];
260
278
  const ipfsService = await promptOptions(
261
- "Select the IPFS pinning service you want to use (default is Ethernity): ",
262
- ipfsServiceOptions,
263
- "Ethernity (best effort)",
279
+ "Which IPFS API do the publish and the dApp write through? (default is 1): ",
280
+ IPFS_OPTIONS,
281
+ IPFS_OPTIONS[0],
264
282
  );
265
283
 
266
- let customUrl, ipfsToken;
267
- if (ipfsService === "Custom IPFS") {
268
- customUrl = await new Promise((resolve) =>
284
+ let ipfsUrl, ipfsToken;
285
+ if (ipfsService === IPFS_OPTIONS[1]) {
286
+ ipfsUrl = await new Promise((resolve) =>
269
287
  rl.question(
270
- "Enter the endpoint URL for the IPFS pinning service you want to use: ",
288
+ "Enter the URL of the IPFS RPC API (e.g. http://127.0.0.1:5001): ",
271
289
  resolve,
272
290
  ),
273
291
  );
274
292
  ipfsToken = await new Promise((resolve) =>
275
293
  rl.question(
276
- "Enter the access token to be used when calling the IPFS pinning service: ",
294
+ "Enter its Authorization header value (empty for none): ",
277
295
  resolve,
278
296
  ),
279
297
  );
280
- } else {
281
- customUrl = "https://ipfs.ethernity.cloud";
282
298
  }
299
+ const ipfs = ipfsSettings(ipfsService, ipfsUrl, ipfsToken);
283
300
 
284
301
  fs.mkdirSync("src/serverless", { recursive: true });
285
302
 
@@ -379,10 +396,10 @@ const main = async () => {
379
396
 
380
397
 
381
398
  writeEnv("BLOCKCHAIN_NETWORK", network);
382
- writeEnv("IPFS_ENDPOINT", customUrl);
399
+ writeEnv("IPFS_ENDPOINT", ipfs.IPFS_ENDPOINT);
400
+ writeEnv("IPFS_TOKEN", ipfs.IPFS_TOKEN);
383
401
  console.log();
384
402
  await configureEsr();
385
- writeEnv("IPFS_TOKEN", ipfsToken || "");
386
403
  writeEnv("VERSION", "v1");
387
404
  console.log();
388
405
  console.log(
@@ -391,4 +408,6 @@ const main = async () => {
391
408
  rl.close();
392
409
  };
393
410
 
394
- main();
411
+ if (require.main === module) main();
412
+
413
+ module.exports = { IPFS_OPTIONS, ipfsSettings, writeEnv };
package/localKubo.js CHANGED
@@ -1,15 +1,15 @@
1
1
  // The Kubo a publish runs for its own upload.
2
2
  //
3
3
  // An image is published by adding it to IPFS and registering its CID. The
4
- // public write API of ipfs.ethernity.cloud is closing, so a publish without an
5
- // IPFS endpoint of its own runs a Kubo in docker (the SDK already requires
6
- // docker), adds the image through that Kubo's HTTP API with the same call the
7
- // public API received (so a build gives the same CIDs as before), peers the
8
- // Kubo with the bootnode, whose mirror pins every registered image and from
9
- // which the certificate extraction service fetches, and stops it once the
10
- // certificate is on chain. The Kubo listens for the API on the loopback
11
- // interface only; its swarm port is published when free, so a publisher with
12
- // a reachable address is also dialed directly.
4
+ // public API of ipfs.ethernity.cloud takes no adds or pins without a writer
5
+ // token, so a publish without an IPFS endpoint of its own runs a Kubo in
6
+ // docker (the SDK already requires docker), adds the image through that Kubo's
7
+ // HTTP API with the same add call (so a build gives the same CIDs on every
8
+ // node), peers the Kubo with the bootnode, whose mirror pins every registered
9
+ // image and from which the certificate extraction service fetches, and stops
10
+ // it once the certificate is on chain. The Kubo listens for the API on the
11
+ // loopback interface only; its swarm port is published when free, so a
12
+ // publisher with a reachable address is also dialed directly.
13
13
  const { execFileSync } = require('child_process');
14
14
  const net = require('net');
15
15
  const axios = require('axios');
@@ -65,6 +65,14 @@ function ownEndpoint(endpoint) {
65
65
  }
66
66
  }
67
67
 
68
+ // The IPFS_ENDPOINT a dApp's template is built with (src/preStart.mjs): the
69
+ // setting when it is an endpoint of the application's own, '' otherwise, on
70
+ // which the dApp delivers its task artefacts through the bootnode's payload
71
+ // intake.
72
+ function dappEndpoint(endpoint) {
73
+ return ownEndpoint(endpoint) ? endpoint : '';
74
+ }
75
+
68
76
  class LocalKubo {
69
77
  constructor(name) {
70
78
  this.container = `ecld-kubo-${String(name || 'publish').replace(/[^A-Za-z0-9_.-]/g, '-')}`;
@@ -202,4 +210,4 @@ class LocalKubo {
202
210
  }
203
211
  }
204
212
 
205
- module.exports = { LocalKubo, ownEndpoint, apiPortIn, PUBLIC_IPFS_HOST };
213
+ module.exports = { LocalKubo, ownEndpoint, dappEndpoint, apiPortIn, PUBLIC_IPFS_HOST };
@@ -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
@@ -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
  etny-swift-stream:
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
  ports:
9
12
  - "9000: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
  }