diff --git a/package.json b/package.json index 24c6561..3b8eddb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@x402r/erc8004", - "version": "0.1.0-alpha.2", + "version": "0.1.0-alpha.1", "description": "Lightweight TypeScript SDK for ERC-8004 (Trustless Agents) — Identity and Reputation registries", "type": "module", "license": "Apache-2.0", diff --git a/src/index.ts b/src/index.ts index e74f887..4d28c51 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,7 +43,6 @@ export { export type { AgentRegistrationFile, CreateRegistrationFileParameters, - FetchRegistrationFileOptions, RegistrationBinding, ResolvedServiceEndpoint, ResolveServiceEndpointParameters, diff --git a/src/registration/fetch.ts b/src/registration/fetch.ts index 7db818c..0ef888b 100644 --- a/src/registration/fetch.ts +++ b/src/registration/fetch.ts @@ -1,47 +1,21 @@ import { parseRegistrationFile } from './parse.js' import type { AgentRegistrationFile } from './types.js' -const DEFAULT_IPFS_GATEWAY = 'https://ipfs.io' - -export interface FetchRegistrationFileOptions { - /** IPFS gateway base URL. Default: `https://ipfs.io`. */ - ipfsGateway?: string -} - /** - * Fetch and validate an Agent Registration File from a URI. + * Fetch and validate an Agent Registration File from an HTTPS URL. * - * Supported schemes: - * - `https://` — fetched via `globalThis.fetch` - * - `data:application/json;base64,...` — decoded inline (fully on-chain storage) - * - `data:application/json,...` — URL-decoded inline - * - `ipfs://` — resolved via public IPFS gateway (configurable) + * Uses `globalThis.fetch` — no external HTTP dependencies required. * - * @throws on unsupported scheme, non-200 response, non-JSON body, or invalid schema + * @throws on non-HTTPS URL, non-200 response, non-JSON body, or invalid schema */ export async function fetchRegistrationFile( uri: string, - options?: FetchRegistrationFileOptions, ): Promise { - if (uri.startsWith('data:')) { - return parseDataUri(uri) + if (!uri.startsWith('https://')) { + throw new Error(`Only HTTPS URIs are supported, got: ${uri.slice(0, 40)}`) } - let fetchUrl: string - if (uri.startsWith('ipfs://')) { - const gateway = options?.ipfsGateway ?? DEFAULT_IPFS_GATEWAY - const base = gateway.endsWith('/') ? gateway.slice(0, -1) : gateway - const path = uri.slice(7) // strip "ipfs://" - fetchUrl = `${base}/ipfs/${path}` - } else if (uri.startsWith('https://')) { - fetchUrl = uri - } else { - throw new Error( - `Unsupported URI scheme (expected https, data, or ipfs): ${uri.slice(0, 40)}`, - ) - } - - const response = await globalThis.fetch(fetchUrl, { + const response = await globalThis.fetch(uri, { signal: AbortSignal.timeout(10_000), }) @@ -65,50 +39,3 @@ export async function fetchRegistrationFile( return parseRegistrationFile(json) } - -// --------------------------------------------------------------------------- -// data: URI parsing -// --------------------------------------------------------------------------- - -function parseDataUri(uri: string): AgentRegistrationFile { - const commaIndex = uri.indexOf(',') - if (commaIndex === -1) { - throw new Error('Malformed data URI: missing comma separator') - } - - const header = uri.slice(5, commaIndex) // strip "data:" prefix - const payload = uri.slice(commaIndex + 1) - - // Validate MIME type — accept application/json with optional charset - const mimeEnd = header.indexOf(';') - const mime = mimeEnd === -1 ? header : header.slice(0, mimeEnd) - if (mime !== 'application/json') { - throw new Error( - `data URI must have application/json MIME type, got: ${mime}`, - ) - } - - const isBase64 = header.endsWith(';base64') - - let decoded: string - try { - decoded = isBase64 ? atob(payload) : decodeURIComponent(payload) - } catch { - throw new Error( - `Failed to decode data URI (${isBase64 ? 'base64' : 'percent-encoded'})`, - ) - } - - if (decoded.length > 1_048_576) { - throw new Error('Registration file exceeds 1 MB size limit') - } - - let json: unknown - try { - json = JSON.parse(decoded) - } catch { - throw new Error('data URI content is not valid JSON') - } - - return parseRegistrationFile(json) -} diff --git a/src/registration/index.ts b/src/registration/index.ts index 651a6dd..997c5a7 100644 --- a/src/registration/index.ts +++ b/src/registration/index.ts @@ -1,8 +1,5 @@ export { createRegistrationFile } from './build.js' -export { - type FetchRegistrationFileOptions, - fetchRegistrationFile, -} from './fetch.js' +export { fetchRegistrationFile } from './fetch.js' export { parseRegistrationFile } from './parse.js' export { resolveServiceEndpoint } from './resolve.js' export { findService, findServices } from './services.js' diff --git a/src/registration/resolve.ts b/src/registration/resolve.ts index c2bfae3..7c55c47 100644 --- a/src/registration/resolve.ts +++ b/src/registration/resolve.ts @@ -31,9 +31,7 @@ export async function resolveServiceEndpoint( let file: AgentRegistrationFile try { - file = await fetchRegistrationFile(agent.agentURI, { - ipfsGateway: parameters.ipfsGateway, - }) + file = await fetchRegistrationFile(agent.agentURI) } catch (error) { throw new Error( `Agent ${parameters.agentId}: ${error instanceof Error ? error.message : String(error)}`, diff --git a/src/registration/types.ts b/src/registration/types.ts index 5e93e88..ac4d8c1 100644 --- a/src/registration/types.ts +++ b/src/registration/types.ts @@ -35,8 +35,6 @@ export interface ResolveServiceEndpointParameters { agentId: bigint serviceName: string registryAddress?: Address - /** IPFS gateway base URL for `ipfs://` agent URIs. Default: `https://ipfs.io`. */ - ipfsGateway?: string } export interface ResolvedServiceEndpoint { diff --git a/tests/registration.test.ts b/tests/registration.test.ts index eb9c7ac..5c8a908 100644 --- a/tests/registration.test.ts +++ b/tests/registration.test.ts @@ -319,132 +319,10 @@ describe('fetchRegistrationFile', () => { expect(result.name).toBe('My Agent') }) - it('throws on unsupported URI scheme', async () => { + it('throws on non-HTTPS URL', async () => { await expect( fetchRegistrationFile('http://example.com/agent.json'), - ).rejects.toThrow('Unsupported URI scheme') - await expect( - fetchRegistrationFile('ftp://example.com/file'), - ).rejects.toThrow('Unsupported URI scheme') - }) - - // --- data: URIs --- - - it('parses base64-encoded data URI', async () => { - const json = JSON.stringify(validPayload()) - const encoded = btoa(json) - const result = await fetchRegistrationFile( - `data:application/json;base64,${encoded}`, - ) - expect(result.name).toBe('My Agent') - }) - - it('parses URL-encoded data URI', async () => { - const json = JSON.stringify(validPayload()) - const encoded = encodeURIComponent(json) - const result = await fetchRegistrationFile( - `data:application/json,${encoded}`, - ) - expect(result.name).toBe('My Agent') - }) - - it('parses data URI with charset parameter', async () => { - const json = JSON.stringify(validPayload()) - const encoded = btoa(json) - const result = await fetchRegistrationFile( - `data:application/json;charset=utf-8;base64,${encoded}`, - ) - expect(result.name).toBe('My Agent') - }) - - it('throws on data URI with wrong MIME type', async () => { - await expect( - fetchRegistrationFile('data:text/plain;base64,aGVsbG8='), - ).rejects.toThrow('application/json MIME type') - }) - - it('throws on data URI with malformed base64', async () => { - await expect( - fetchRegistrationFile('data:application/json;base64,!!!invalid'), - ).rejects.toThrow('Failed to decode') - }) - - it('throws on data URI with invalid JSON', async () => { - const encoded = btoa('not json') - await expect( - fetchRegistrationFile(`data:application/json;base64,${encoded}`), - ).rejects.toThrow('not valid JSON') - }) - - it('throws on data URI exceeding size limit', async () => { - const big = JSON.stringify({ - ...validPayload(), - pad: 'x'.repeat(1_048_577), - }) - const encoded = btoa(big) - await expect( - fetchRegistrationFile(`data:application/json;base64,${encoded}`), - ).rejects.toThrow('exceeds 1 MB') - }) - - it('throws on data URI missing comma', async () => { - await expect( - fetchRegistrationFile('data:application/json;base64'), - ).rejects.toThrow('missing comma') - }) - - // --- ipfs:// URIs --- - - it('rewrites ipfs:// to default gateway', async () => { - const fetchSpy = mockFetch(JSON.stringify(validPayload())) - vi.stubGlobal('fetch', fetchSpy) - - await fetchRegistrationFile('ipfs://QmTest123') - - expect(fetchSpy).toHaveBeenCalledWith( - 'https://ipfs.io/ipfs/QmTest123', - expect.anything(), - ) - }) - - it('accepts custom ipfsGateway', async () => { - const fetchSpy = mockFetch(JSON.stringify(validPayload())) - vi.stubGlobal('fetch', fetchSpy) - - await fetchRegistrationFile('ipfs://QmTest123', { - ipfsGateway: 'https://gateway.pinata.cloud', - }) - - expect(fetchSpy).toHaveBeenCalledWith( - 'https://gateway.pinata.cloud/ipfs/QmTest123', - expect.anything(), - ) - }) - - it('handles ipfs:// with path after CID', async () => { - const fetchSpy = mockFetch(JSON.stringify(validPayload())) - vi.stubGlobal('fetch', fetchSpy) - - await fetchRegistrationFile('ipfs://QmTest123/metadata.json') - - expect(fetchSpy).toHaveBeenCalledWith( - 'https://ipfs.io/ipfs/QmTest123/metadata.json', - expect.anything(), - ) - }) - - it('handles ipfsGateway with trailing slash', async () => { - const fetchSpy = mockFetch(JSON.stringify(validPayload())) - vi.stubGlobal('fetch', fetchSpy) - - await fetchRegistrationFile('ipfs://QmTest123', { - ipfsGateway: 'https://ipfs.io/', - }) - - expect(fetchSpy).toHaveBeenCalledWith( - 'https://ipfs.io/ipfs/QmTest123', - expect.anything(), - ) + ).rejects.toThrow('Only HTTPS URIs are supported') }) it('throws on non-200 response', async () => {