@web3-storage/content-claims
Version:
Implementation of the Content Claims Protocol.
129 lines (128 loc) • 4.6 kB
JavaScript
/* 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;
};