Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/toolkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
43 changes: 36 additions & 7 deletions packages/toolkit/src/repository/repository.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -453,26 +454,54 @@ 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.
* @throws Error when the provided hash is invalid.
*/
public async list(hash: string): Promise<IPFSEntry[]> {
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 };
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading