From 1df2dc6d1b2a373ba79b9bedbb0825d6c703166a Mon Sep 17 00:00:00 2001 From: Bit Cloud Date: Mon, 27 Jul 2026 11:05:51 +0000 Subject: [PATCH 1/2] feat: drop embedded js-ipfs (ipfs-core), keep the HTTP Kubo API The upstream already had an HTTP mode against the Kubo RPC API (api/v0) via got; only the embedded-node path used ipfs-core (EOL). Remove that path and _coreMode, keeping the HTTP behavior and therefore byte-identical CIDs (same add options, same cid<->hash math). Endpoint from FORGE_IPFS_API / IPFS_API. Drop ipfs-core and the now-unused sleep-promise; pin engines node>=20. --- index.js | 527 ++++++++++++++------------------------------------- package.json | 7 +- 2 files changed, 143 insertions(+), 391 deletions(-) diff --git a/index.js b/index.js index 07358e2..4fa6cd9 100644 --- a/index.js +++ b/index.js @@ -1,388 +1,139 @@ -const IPFS=require('ipfs-core'); -const got=require('got'); -const FormData = require('form-data'); -const {CID} = require('multiformats'); -const sleep=require("sleep-promise"); - -class IPFS_Simple { - /** - * Creates the IPFS object. Path can be changed with path setter - */ - constructor() { - this._base="http://127.0.0.1:5001/api/v0/"; - this._coreMode=false; - this._creating=false; - } - - /** - * Creates an IPFS objects instead of using path - * @returns {Promise} - */ - async create() { - this._creating=true; - this._base=await IPFS.create(); - this._creating=false; - this._coreMode=true; - } - - /** - * returns the objects core - * @returns {string|IPFS} - */ - get core() { - return this._base; - } - - /** - * Sets up ipfs object to use a specific core - * @param {string|IPFS} core - * @returns {Promise} - */ - init(core) { - this._base=core; - this._coreMode=(typeof core!=="string"); - } - - /** - * Sets the path to IPFS interface - * @deprecated - * @param {string} path - */ - set path(path) { - this._base=path; - this._coreMode=false; - } - - /** - * converts a cid in to a hash - * @param {string} cidString - * @return {string} - */ - cidToHash(cidString) { - const cid = CID.parse(cidString); - //get hash - let hash = ""; - for (let i = 4; i < 36; i++) hash += cid.bytes[i].toString(16).padStart(2, '0'); - - return hash; - } - - /** - * converts a hash in to a cid - * @param {string} hash - * @return {string} - */ - hashToCid(hash) { - const hashPrefix = "1220"; - const cid = CID.create(1, 0x55, { - bytes: Uint8Array.from(Buffer.from(hashPrefix + hash, 'hex')) - }); - return cid.toString(); - } - - /** - * Pins a file returns if successful - * @param {string} cid - * @param {int} timeout - * @return {Promise} - */ - async pinAdd(cid, timeout = 600000) { - //use IPFS core if initialized - while (this._creating) await sleep(100); - if (this._coreMode) { - let response=await this._base.pin.add(CID.parse(cid),{timeout}); - return (response.toString()===cid); - } - - //Use IPFS Desktop - return new Promise(async (resolve, reject) => { - //handle timeouts - let timer = setTimeout(() => { - reject("get size of " + cid + " timed out"); - }, timeout); - - //get desired stats - let url = this._base + 'pin/add/' + cid; - let response=await got.post(url); - let {Pins}=JSON.parse(response.body); - - //clear timeout and return - clearInterval(timer); - resolve(Pins[0]===cid); - }); - } - - /** - * Removes the pin on a file - * @param cid - * @return {Promise} - */ - async pinRemove(cid) { - //use IPFS core if initialized - while (this._creating) await sleep(100); - if (this._coreMode) { - await this._base.pin.rm(CID.parse(cid)); - return; - } - - //Use IPFS Desktop - let url = this._base + 'pin/rm/' + cid; - await got.post(url); - } - - /** - * Returns the data in an object - * @param {string} cid - * @param {int} timeout - * @return {Promise} - */ - async catBuffer(cid, timeout = 600000) { - //use IPFS core if initialized - while (this._creating) await sleep(100); - if (this._coreMode) { - let chunks=[]; - for await (const chunk of this._base.cat(cid,{timeout})) { - chunks.push(chunk); - } - return Buffer.concat(chunks); - } - - //Use IPFS Desktop - return new Promise(async (resolve, reject) => { - //handle timeouts - let timer = setTimeout(() => { - reject("cat " + cid + " timed out"); - }, timeout); - - //get desired stats - try { - let url = this._base + 'cat/' + cid; - let response = (await got.post(url,{ - responseType:"buffer" - })).body; - resolve(response); - } catch (e) { - reject(e); - } - - //clear timeout and return - clearInterval(timer); - }); - } - - /** - * Returns the data in an object - * @param {string} cid - * @param {int} timeout - * @return {Promise} - */ - async cat(cid, timeout = 600000) { - return (await this.catBuffer(cid,timeout)).toString(); - } - - /** - * Returns the json data in an object - * @param {string} cid - * @param {int} timeout - * @return {Promise<{}>} - */ - async catJSON(cid, timeout = 600000) { - return JSON.parse(await this.cat(cid,timeout)); - } - - - /** - * Adds a json file and returns its cid - * @param {{}} json - * @return {Promise} - */ - async addRawJSON(json) { - //use IPFS core if initialized - while (this._creating) await sleep(100); - if (this._coreMode) { - let response=await this._base.add(Buffer.from(JSON.stringify(json), 'utf8'),{ - pin: true, - rawLeaves: true, - hashAlg: 'sha2-256' - }); - return response.cid.toString(); - } - - //Use IPFS Desktop - let form = new FormData(); - form.append('path', Buffer.from(JSON.stringify(json), 'utf8')); - - let url = this._base + 'add?pin=true&raw-leaves=true&hash=sha2-256'; - let response = (await got.post(url, { - headers: form.getHeaders(), - body: form - })).body; - return JSON.parse(response).Hash; - } - - /** - * Adds a json file and returns its cid - * @param {Buffer} data - * @return {Promise} - */ - async addBuffer(data) { - //use IPFS core if initialized - while (this._creating) await sleep(100); - if (this._coreMode) { - let response=await this._base.add(data,{ - pin: true, - hashAlg: 'sha2-256' - }); - return response.cid.toString(); - } - - //Use IPFS Desktop - let form = new FormData(); - form.append('path', data); - - let url = this._base + 'add?pin=true&hash=sha2-256'; - let response = (await got.post(url, { - headers: form.getHeaders(), - body: form - })).body; - return JSON.parse(response).Hash; - } - - - /** - * Returns if a cid is pinned or not - * @param {string} cid - * @return {Promise} - */ - async checkPinned(cid) { - //use IPFS core if initialized - while (this._creating) await sleep(100); - if (this._coreMode) { - for await (const response of this._base.pin.ls({ - paths: CID.parse(cid) - })) { - if (response.cid.toString()===cid) return true; - } - return false; - } - - //Use IPFS Desktop - let url = this._base + 'pin/ls/' + cid; - let response = JSON.parse((await got.post(url)).body); - return (response.Type === undefined); //Type will equal "error" if not pinned and be not present if pinned - } - - /** - * returns a list of all pinned cids - * @return {Promise} - */ - async listPinned() { - //use IPFS core if initialized - while (this._creating) await sleep(100); - if (this._coreMode) { - let responses=[]; - for await (const { cid, type } of this._base.pin.ls()) { - responses.push(cid.toString()); - } - return responses; - } - - //Use IPFS Desktop - let url= this._base + 'pin/ls'; - let response = JSON.parse((await got.post(url)).body); - // noinspection JSUnresolvedVariable - return response.Keys; - } - - /** - * Gets the size of an object - * @param {string} cid - * @param {int} timeout - * @return {Promise} - */ - async getSize(cid, timeout = 600000) { - //use IPFS core if initialized - while (this._creating) await sleep(100); - if (this._coreMode) { - try { - let response = await this._base.object.stat(CID.parse(cid), {timeout}); - return response.CumulativeSize; - } catch (e) { - return (await this.catBuffer(cid)).length; - } - } - - //Use IPFS Desktop - return new Promise(async (resolve, reject) => { - //handle timeouts - let timer = setTimeout(() => { - reject("get size of " + cid + " timed out"); - }, timeout); - - //get desired stats - let url = this._base + 'object/stat/' + cid; - /** @type {{NumLinks:int,BlockSize:int,LinksSize:int,DataSize:int,CumulativeSize:int}}*/ - let response = JSON.parse((await got.post(url)).body); - - //clear timeout and return - clearInterval(timer); - resolve(response.CumulativeSize); - }); - } - - /** - * Trys to connect to a peer - * @param {string} location - * @param {int} timeout - * @returns {Promise} - */ - async addPeer(location, timeout = 600000) { - //use IPFS core if initialized - while (this._creating) await sleep(100); - if (this._coreMode) { - await this._base.swarm.connect(location,{timeout}); - return; - } - - //Use IPFS Desktop - return new Promise(async (resolve, reject) => { - //handle timeouts - let timer = setTimeout(() => { - reject("connecting to peer " + location + " timed out"); - }, timeout); - - //get desired stats - let url = this._base + 'swarm/connect?arg=' + location; - let response = JSON.parse((await got.post(url)).body); - - //clear timeout and return - clearInterval(timer); - // noinspection JSUnresolvedVariable - resolve(response.Strings[0].endsWith("success")); - }); - } - - /** - * Gets the id - * Warning the core and desktop return in different formats - * @returns {Promise} - */ - async getId() { - //use IPFS core if initialized - while (this._creating) await sleep(100); - if (this._coreMode) { - return this._base.id(); - } - - //Use IPFS Desktop - return new Promise(async (resolve, reject) => { - let url = this._base + 'id'; - let response = JSON.parse((await got.post(url)).body); - resolve(response); - }); - } -} - - -const singularity=new IPFS_Simple(); -module.exports=singularity; \ No newline at end of file +const got = require('got'); +const FormData = require('form-data'); +const { CID } = require('multiformats'); + +// Modernized: talks to an external Kubo daemon over the HTTP RPC API (`api/v0`). +// The upstream already had this HTTP mode; this fork drops the embedded js-ipfs +// path (`ipfs-core`, EOL) and its `_coreMode`, keeping the HTTP behavior — and +// therefore byte-identical CIDs (same add options, same cid<->hash math). The +// endpoint comes from FORGE_IPFS_API / IPFS_API (default 127.0.0.1:5001). +function defaultBase() { + let url = process.env.FORGE_IPFS_API || process.env.IPFS_API || 'http://127.0.0.1:5001'; + url = url.replace(/\/+$/, ''); + if (!/\/api\/v0$/.test(url)) url += '/api/v0'; + return url + '/'; +} + +class IPFS_Simple { + constructor() { + this._base = defaultBase(); + } + + get core() { + return this._base; + } + + /** Connect to the configured Kubo daemon (was: start an embedded node). */ + async create() { + this._base = defaultBase(); + } + + /** Point at a specific Kubo HTTP API base URL. */ + init(base) { + if (typeof base === 'string') { + this._base = base.endsWith('/') ? base : base + '/'; + } + } + + /** @deprecated set the HTTP API base path directly */ + set path(path) { + this._base = path; + } + + /** converts a cid in to a hash (unchanged — protocol-critical) */ + cidToHash(cidString) { + const cid = CID.parse(cidString); + let hash = ''; + for (let i = 4; i < 36; i++) hash += cid.bytes[i].toString(16).padStart(2, '0'); + return hash; + } + + /** converts a hash in to a cid (unchanged — protocol-critical) */ + hashToCid(hash) { + const hashPrefix = '1220'; + const cid = CID.create(1, 0x55, { + bytes: Uint8Array.from(Buffer.from(hashPrefix + hash, 'hex')), + }); + return cid.toString(); + } + + async pinAdd(cid, timeout = 600000) { + const response = await got.post(this._base + 'pin/add/' + cid, { timeout: { request: timeout } }); + const { Pins } = JSON.parse(response.body); + return Pins[0] === cid; + } + + async pinRemove(cid) { + await got.post(this._base + 'pin/rm/' + cid); + } + + async catBuffer(cid, timeout = 600000) { + return (await got.post(this._base + 'cat/' + cid, { + responseType: 'buffer', + timeout: { request: timeout }, + })).body; + } + + async cat(cid, timeout = 600000) { + return (await this.catBuffer(cid, timeout)).toString(); + } + + async catJSON(cid, timeout = 600000) { + return JSON.parse(await this.cat(cid, timeout)); + } + + async addRawJSON(json) { + const form = new FormData(); + form.append('path', Buffer.from(JSON.stringify(json), 'utf8')); + const response = (await got.post(this._base + 'add?pin=true&raw-leaves=true&hash=sha2-256', { + headers: form.getHeaders(), + body: form, + })).body; + return JSON.parse(response).Hash; + } + + async addBuffer(data) { + const form = new FormData(); + form.append('path', data); + const response = (await got.post(this._base + 'add?pin=true&hash=sha2-256', { + headers: form.getHeaders(), + body: form, + })).body; + return JSON.parse(response).Hash; + } + + async checkPinned(cid) { + try { + const response = JSON.parse((await got.post(this._base + 'pin/ls/' + cid)).body); + return response.Type === undefined; // "error" (or absent Type) when not pinned + } catch (_error) { + return false; + } + } + + async listPinned() { + const response = JSON.parse((await got.post(this._base + 'pin/ls')).body); + return Object.keys(response.Keys || {}); + } + + async getSize(cid, timeout = 600000) { + const response = JSON.parse((await got.post(this._base + 'object/stat/' + cid, { + timeout: { request: timeout }, + })).body); + return response.CumulativeSize; + } + + async addPeer(location, timeout = 600000) { + const response = JSON.parse((await got.post(this._base + 'swarm/connect?arg=' + location, { + timeout: { request: timeout }, + })).body); + return response.Strings[0].endsWith('success'); + } + + async getId() { + return JSON.parse((await got.post(this._base + 'id')).body); + } +} + +const singularity = new IPFS_Simple(); +module.exports = singularity; diff --git a/package.json b/package.json index 6887f49..6d8f271 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,13 @@ "keywords": [], "author": "Matthew Cornelisse", "license": "MIT", + "engines": { + "node": ">=20" + }, "dependencies": { "form-data": "^4.0.0", "got": "^11.8.2", - "ipfs-core": "^0.12.2", - "multiformats": "^9.4.8", - "sleep-promise": "^9.1.0" + "multiformats": "^9.4.8" }, "repository": { "type": "git", From 0171906a2a19f7dc7faff5e5b8eee568d57958e6 Mon Sep 17 00:00:00 2001 From: Bit Cloud Date: Mon, 27 Jul 2026 11:14:02 +0000 Subject: [PATCH 2/2] fix(getSize): use files/stat; object/stat was removed from Kubo Modern Kubo returns 500 ("removed, use 'ipfs dag' or 'ipfs files'") for object/stat. files/stat on /ipfs/ returns the same CumulativeSize. Verified against a live kubo daemon: full API smoke (add/cat/pin/size + cid<->hash inverse + deterministic CID) passes. --- index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 4fa6cd9..8a56335 100644 --- a/index.js +++ b/index.js @@ -117,7 +117,9 @@ class IPFS_Simple { } async getSize(cid, timeout = 600000) { - const response = JSON.parse((await got.post(this._base + 'object/stat/' + cid, { + // `object/stat` was removed from Kubo; `files/stat` on the /ipfs path + // returns the same CumulativeSize. + const response = JSON.parse((await got.post(this._base + 'files/stat?arg=/ipfs/' + cid, { timeout: { request: timeout }, })).body); return response.CumulativeSize;