diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index f5ce69659f..0f9a32cc53 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -29,6 +29,7 @@ "dependencies": { "@dappnode/types": "workspace:^0.1.0", "@ipld/car": "^5.4.0", + "@ipld/dag-pb": "^4.1.2", "esm": "^3.2.25", "ethers": "^6.15.0", "graphql": "^16.6.0", diff --git a/packages/toolkit/src/repository/repository.ts b/packages/toolkit/src/repository/repository.ts index c8eaab7820..2e0ace0414 100644 --- a/packages/toolkit/src/repository/repository.ts +++ b/packages/toolkit/src/repository/repository.ts @@ -1,6 +1,7 @@ import * as isIPFS from "is-ipfs"; import { CID, IPFSEntry } from "kubo-rpc-client"; import { CarReader } from "@ipld/car"; +import { decode as decodeDagPb } from "@ipld/dag-pb"; import { recursive as exporter } from "ipfs-unixfs-exporter"; import { Version } from "multiformats"; import path from "path"; @@ -453,10 +454,13 @@ export class DappnodeRepository extends ApmRepository { } /** - * Lists the contents of a directory pointed by the given hash using IPFS dag-json. + * Lists the contents of a directory pointed by the given hash. * Returns entries with individual file CIDs (required for signature verification). * - * TODO: research why the size is different, i.e for the hash QmWcJrobqhHF7GWpqEbxdv2cWCCXbACmq85Hh7aJ1eu8rn Tsize is 64461521 and size is 64446140 + * Uses ?format=raw to fetch the raw dag-pb block and decodes it client-side. + * This is compatible with Kubo v0.40+ where cross-codec conversion (dag-pb → dag-json) + * is disabled by default per IPIP-524. + * Falls back to dag-json for older gateways that may not serve raw blocks. * * @param hash - The content identifier (CID) of the directory. * @returns An array of entries in the directory. @@ -464,15 +468,40 @@ export class DappnodeRepository extends ApmRepository { */ public async list(hash: string): Promise { const cidStr = this.sanitizeIpfsPath(hash.toString()); - const url = `${this.gatewayUrl}/ipfs/${cidStr}?format=dag-json`; - const res = await fetch(url, { + + // Primary: fetch raw dag-pb block and decode client-side (works on all Trustless Gateways) + const rawUrl = `${this.gatewayUrl}/ipfs/${cidStr}?format=raw`; + const rawRes = await fetch(rawUrl, { + headers: { Accept: "application/vnd.ipld.raw" } + }); + + if (rawRes.ok) { + const bytes = new Uint8Array(await rawRes.arrayBuffer()); + const pbNode = decodeDagPb(bytes); + + if (!pbNode.Links || pbNode.Links.length === 0) { + throw new Error(`Invalid IPFS directory CID ${cidStr}`); + } + + return pbNode.Links.map((link) => ({ + type: "file" as const, + cid: link.Hash, + name: link.Name ?? "", + path: `${link.Hash.toString()}/${link.Name ?? ""}`, + size: link.Tsize ?? 0 + })); + } + + // Fallback: dag-json for older gateways that predate IPIP-524 + const dagJsonUrl = `${this.gatewayUrl}/ipfs/${cidStr}?format=dag-json`; + const dagJsonRes = await fetch(dagJsonUrl, { headers: { Accept: "application/vnd.ipld.dag-json" } }); - if (!res.ok) { - throw new Error(`Failed to list directory ${cidStr}: ${res.status} ${res.statusText}`); + if (!dagJsonRes.ok) { + throw new Error(`Failed to list directory ${cidStr}: ${dagJsonRes.status} ${dagJsonRes.statusText}`); } - const dagJson = (await res.json()) as { + const dagJson = (await dagJsonRes.json()) as { Links?: Array<{ Name: string; Hash: { "/": string }; diff --git a/yarn.lock b/yarn.lock index dca2afbf44..f0f736989a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1561,6 +1561,7 @@ __metadata: dependencies: "@dappnode/types": "workspace:^0.1.0" "@ipld/car": "npm:^5.4.0" + "@ipld/dag-pb": "npm:^4.1.2" "@types/mocha": "npm:^10" "@types/semver": "npm:^7.3.13" esm: "npm:^3.2.25"