Skip to content
Merged
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
80 changes: 65 additions & 15 deletions lib/contracts/erc8004.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,27 +212,77 @@ export async function detectWriterType(
}
}

const MAX_URI_BYTES = 50 * 1024; // 50 KB size limit
const FETCH_TIMEOUT_MS = 5000; // 5-second timeout

/**
* Resolve an agent URI string to a parsed JSON object.
* Handles raw JSON, data: URIs (base64 + URL-encoded), https://, and ipfs://.
* Includes timeout, size limits, and response validation for safety.
*/
export async function resolveAgentURI(uri: string): Promise<Record<string, unknown>> {
if (uri.startsWith("{")) {
return JSON.parse(uri);
}
if (uri.startsWith("data:")) {
const comma = uri.indexOf(",");
const payload = comma >= 0 ? uri.slice(comma + 1) : uri;
return JSON.parse(
uri.includes("base64") ? atob(payload) : decodeURIComponent(payload),
);
try {
if (uri.startsWith("{")) {
if (uri.length > MAX_URI_BYTES) return {};
return JSON.parse(uri);
}
if (uri.startsWith("data:")) {
const comma = uri.indexOf(",");
const payload = comma >= 0 ? uri.slice(comma + 1) : uri;
if (payload.length > MAX_URI_BYTES) return {};
return JSON.parse(
uri.includes("base64") ? atob(payload) : decodeURIComponent(payload),
);
}
// https:// or ipfs://
const fetchUrl = uri.startsWith("ipfs://")
? uri.replace("ipfs://", "https://ipfs.io/ipfs/")
: uri;

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const res = await fetch(fetchUrl, { signal: controller.signal });
if (!res.ok) return {};

// Reject early if Content-Length exceeds limit
const contentLength = res.headers.get("content-length");
if (contentLength && parseInt(contentLength, 10) > MAX_URI_BYTES) return {};

// Stream body with size cap to avoid buffering oversized responses
const reader = res.body?.getReader();
if (!reader) return {};
const chunks: Uint8Array[] = [];
let totalBytes = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
totalBytes += value.byteLength;
if (totalBytes > MAX_URI_BYTES) {
reader.cancel();
return {};
}
chunks.push(value);
}
let bytes: Uint8Array;
if (chunks.length === 1) {
bytes = chunks[0];
} else {
bytes = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
}
const text = new TextDecoder().decode(bytes);
return JSON.parse(text);
} finally {
clearTimeout(timeout);
}
} catch {
return {};
}
// https:// or ipfs://
const fetchUrl = uri.startsWith("ipfs://")
? uri.replace("ipfs://", "https://ipfs.io/ipfs/")
: uri;
const res = await fetch(fetchUrl);
return (await res.json()) as Record<string, unknown>;
}

/**
Expand Down
Loading