Skip to content
Open
46 changes: 46 additions & 0 deletions app/features/metadata/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { vi } from 'vitest';

import { getProxiedUri } from '../utils';

// A well-known valid CIDv0 (contains Hello World)
const VALID_CID_V0 = 'QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u';
// A well-known valid CIDv1 (contains Hello World)
const VALID_CID_V1 = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3ek5bfx73d7h4x7bgd35y2nuq';

describe('getProxiedUri', () => {
const originalEnv = process.env;

Expand Down Expand Up @@ -38,6 +43,47 @@ describe('getProxiedUri', () => {
expect(getProxiedUri(uri)).toBe('/api/metadata/proxy?uri=https%3A%2F%2Fexample.com');
});

it('returns the rewritten HTTP gateway URI when proxy is not enabled and protocol is ipfs (CIDv0)', () => {
process.env.NEXT_PUBLIC_METADATA_ENABLED = 'false';
const uri = `ipfs://${VALID_CID_V0}`;
expect(getProxiedUri(uri)).toBe(`https://ipfs.io/ipfs/${VALID_CID_V0}`);
});

it('returns the rewritten HTTP gateway URI when proxy is not enabled and protocol is ipfs (CIDv1)', () => {
process.env.NEXT_PUBLIC_METADATA_ENABLED = 'false';
const uri = `ipfs://${VALID_CID_V1}`;
expect(getProxiedUri(uri)).toBe(`https://ipfs.io/ipfs/${VALID_CID_V1}`);
});

it('returns proxied HTTP gateway URI when proxy is enabled and protocol is ipfs (CIDv0)', () => {
process.env.NEXT_PUBLIC_METADATA_ENABLED = 'true';
const uri = `ipfs://${VALID_CID_V0}`;
expect(getProxiedUri(uri)).toBe(`/api/metadata/proxy?uri=${encodeURIComponent(`https://ipfs.io/ipfs/${VALID_CID_V0}`)}`);
});

it('returns proxied HTTP gateway URI when proxy is enabled and protocol is ipfs (CIDv1)', () => {
process.env.NEXT_PUBLIC_METADATA_ENABLED = 'true';
const uri = `ipfs://${VALID_CID_V1}`;
expect(getProxiedUri(uri)).toBe(`/api/metadata/proxy?uri=${encodeURIComponent(`https://ipfs.io/ipfs/${VALID_CID_V1}`)}`);
});

it('returns proxied HTTP gateway URI handling ipfs/ prefix when proxy is enabled and protocol is ipfs (CIDv0)', () => {
process.env.NEXT_PUBLIC_METADATA_ENABLED = 'true';
const uri = `ipfs://ipfs/${VALID_CID_V0}`;
expect(getProxiedUri(uri)).toBe(`/api/metadata/proxy?uri=${encodeURIComponent(`https://ipfs.io/ipfs/${VALID_CID_V0}`)}`);
});

it('returns proxied HTTP gateway URI handling ipfs/ prefix when proxy is enabled and protocol is ipfs (CIDv1)', () => {
process.env.NEXT_PUBLIC_METADATA_ENABLED = 'true';
const uri = `ipfs://ipfs/${VALID_CID_V1}`;
expect(getProxiedUri(uri)).toBe(`/api/metadata/proxy?uri=${encodeURIComponent(`https://ipfs.io/ipfs/${VALID_CID_V1}`)}`);
});

it('returns empty string for malformed IPFS CIDs', () => {
process.env.NEXT_PUBLIC_METADATA_ENABLED = 'true';
expect(getProxiedUri('ipfs://not-a-valid-cid')).toBe('');
});

it('returns empty string when empty string is passed', () => {
process.env.NEXT_PUBLIC_METADATA_ENABLED = 'true';
expect(getProxiedUri('')).toBe('');
Expand Down
40 changes: 36 additions & 4 deletions app/features/metadata/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export const getProxiedUri = (uri: string): string | '' => {
const isProxyEnabled = process.env.NEXT_PUBLIC_METADATA_ENABLED === 'true';
import Logger from '@utils/logger';
import { CID } from 'multiformats/cid';

if (!isProxyEnabled) return uri;
const IPFS_GATEWAY = 'https://ipfs.io/ipfs';

export const getProxiedUri = (uri: string): string | '' => {
// handle empty addresses as that is likely the case for metadata
if (uri === '') return '';

Expand All @@ -13,7 +14,38 @@ export const getProxiedUri = (uri: string): string | '' => {
throw new Error(`Could not construct URL for "${uri}"`);
}

const isProxyEnabled = process.env.NEXT_PUBLIC_METADATA_ENABLED === 'true';

if (url.protocol === 'ipfs:') {
Comment thread
holps-7 marked this conversation as resolved.
const gatewayUri = resolveIpfsUri(url);
if (gatewayUri === '') return '';
return isProxyEnabled ? proxyUri(gatewayUri) : gatewayUri;
}

if (!isProxyEnabled) return uri;

if (!['http:', 'https:'].includes(url.protocol)) return uri;

return `/api/metadata/proxy?uri=${encodeURIComponent(uri)}`;
return proxyUri(uri);
};

const resolveIpfsUri = (url: URL): string => {
// eslint-disable-next-line no-restricted-syntax -- Strips redundant "ipfs/" prefix from the path for a clean gateway URL.
const path = (url.host + url.pathname).replace(/^ipfs\//, '');
Comment thread
holps-7 marked this conversation as resolved.
if (!verifyCID(path)) {
Logger.warn(`[metadata] Cannot fetch a malformed CID: ${path}`);
return '';
}
return `${IPFS_GATEWAY}/${path}${url.search}`;
Comment thread
holps-7 marked this conversation as resolved.
};

const proxyUri = (uri: string): string => `/api/metadata/proxy?uri=${encodeURIComponent(uri)}`;

const verifyCID = (cid: string): boolean => {
try {
CID.parse(cid);
return true;
} catch {
return false;
}
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"lighthouse-sdk": "2.0.1",
"micromatch": "4.0.8",
"moment": "2.29.4",
"multiformats": "13.4.2",
"next": "14.2.35",
"node-fetch": "3.3.2",
"p-limit": "3.1.0",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.