UNPKG

@web3-storage/content-claims

Version:
129 lines (128 loc) 4.6 kB
/* eslint-env browser */ import { extract as extractDelegation } from '@ucanto/core/delegation'; import { connect, invoke, delegate } from '@ucanto/client'; import { CAR, HTTP } from '@ucanto/transport'; import { sha256 } from 'multiformats/hashes/sha2'; import { equals } from 'multiformats/bytes'; import { base58btc } from 'multiformats/bases/base58'; import { CARReaderStream } from 'carstream/reader'; import * as Assert from '../capability/assert.js'; import { decode as decodeDigest } from 'multiformats/hashes/digest'; export const serviceURL = new URL('https://claims.web3.storage'); /** @type {import('@ucanto/interface').Principal} */ export const servicePrincipal = { did: () => 'did:web:claims.web3.storage' }; /** @type {import('@ucanto/interface').ConnectionView<import('../server/service/api.js').Service>} */ export const connection = connect({ id: servicePrincipal, codec: CAR.outbound, channel: HTTP.open({ url: serviceURL, method: 'POST' }) }); export { connect, invoke, delegate, CAR, HTTP }; const assertCapMap = { [Assert.location.can]: Assert.location, [Assert.partition.can]: Assert.partition, [Assert.inclusion.can]: Assert.inclusion, [Assert.index.can]: Assert.index, [Assert.relation.can]: Assert.relation, [Assert.equals.can]: Assert.equals }; /** * @param {import('@ucanto/interface').Capability} cap * @returns {cap is import('../server/api.js').AnyAssertCap} */ const isAssertCap = cap => Object.keys(assertCapMap).includes(cap.can) && 'nb' in cap && typeof cap.nb === 'object' && 'content' in cap.nb; /** * @param {Uint8Array} bytes * @returns {Promise<import('./api.js').Claim>} */ export const decode = async (bytes) => { const delegation = await extractDelegation(bytes); if (delegation.error) { throw new Error('failed to decode claim', { cause: delegation.error }); } return decodeDelegation(delegation.ok); }; /** * @param {import('@ucanto/interface').Delegation} delegation * @returns {Promise<import('./api.js').Claim>} */ export const decodeDelegation = async (delegation) => { const cap = delegation.capabilities[0]; if (!isAssertCap(cap)) { throw new Error('invalid claim'); } // @ts-expect-error const parsedCap = assertCapMap[cap.can].create({ with: cap.with, nb: cap.nb }); // @ts-expect-error return { ...parsedCap.nb, type: parsedCap.can, delegation: () => delegation }; }; /** * Fetch a CAR archive of claims from the service. Note: no verification is * performed on the response data. * * @typedef {{ * walk?: Array<'parts'|'includes'|'children'> * serviceURL?: URL * }} FetchOptions * @param {import('multiformats').MultihashDigest} content * @param {FetchOptions} [options] */ export const fetch = async (content, options) => { const path = `/claims/multihash/${base58btc.encode(content.bytes)}`; const url = new URL(path, options?.serviceURL ?? serviceURL); if (options?.walk) url.searchParams.set('walk', options.walk.join(',')); return globalThis.fetch(url); }; /** * Read content claims from the service for the given content CID. * * @param {import('multiformats').MultihashDigest} content * @param {FetchOptions} [options] * @returns {Promise<import('./api.js').Claim[]>} */ export const read = async (content, options) => { const res = await fetch(content, options); if (!res.ok) throw new Error(`unexpected service status: ${res.status}`, { cause: await res.text() }); if (!res.body) throw new Error('missing response body'); /** @type {import('./api.js').Claim[]} */ const claims = []; try { await res.body .pipeThrough(new CARReaderStream()) .pipeTo(new WritableStream({ async write(block) { const digest = await sha256.digest(block.bytes); if (!equals(block.cid.multihash.bytes, digest.bytes)) { throw new Error(`hash verification failed: ${block.cid}`); } const claim = await decode(block.bytes); claims.push(claim); } })); } catch ( /** @type {any} */err) { if (!claims.length && err.message === 'unexpected end of data') { return claims; } throw err; } return claims; }; /** * * @param {import('./api.js').Claim} claim * @returns */ export const contentMultihash = (claim) => { return 'digest' in claim.content ? decodeDigest(claim.content.digest) : claim.content.multihash; };