From fed214a095feade68630423a057d94e9895fa230 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Fri, 26 Apr 2024 10:10:09 -0700 Subject: [PATCH] fix: restrict store API to CARs --- packages/capabilities/src/store.js | 11 +++- packages/capabilities/src/types.ts | 4 +- .../test/capabilities/store.test.js | 26 +++++---- packages/upload-api/src/types.ts | 2 +- .../test/handlers/space-info.spec.js | 8 ++- packages/upload-api/test/handlers/store.js | 55 +++++++++++++++++++ packages/upload-client/src/store.js | 2 +- packages/w3up-client/src/capability/store.js | 2 +- 8 files changed, 89 insertions(+), 21 deletions(-) diff --git a/packages/capabilities/src/store.js b/packages/capabilities/src/store.js index 6582b50f0..9e143d068 100644 --- a/packages/capabilities/src/store.js +++ b/packages/capabilities/src/store.js @@ -11,6 +11,11 @@ import { capability, Link, Schema, ok, fail } from '@ucanto/validator' import { equalLink, equalWith, SpaceDID } from './utils.js' +// @see https://github.com/multiformats/multicodec/blob/master/table.csv#L140 +export const code = 0x0202 + +export const CARLink = Schema.link({ code, version: 1 }) + /** * Capability can only be delegated (but not invoked) allowing audience to * derived any `store/` prefixed capability for the (memory) space identified @@ -46,7 +51,7 @@ export const add = capability({ * for this exact CAR file for agent to PUT or POST it. Attempt to write * any other content will fail. */ - link: Link, + link: CARLink, /** * Size of the CAR file to be stored. Service will provision write target * for this exact size. Attempt to write a larger CAR file will fail. @@ -94,7 +99,7 @@ export const get = capability({ /** * shard CID to fetch info about. */ - link: Link.optional(), + link: CARLink.optional(), }), derives: equalLink, }) @@ -114,7 +119,7 @@ export const remove = capability({ /** * CID of the CAR file to be removed from the store. */ - link: Link, + link: CARLink, }), derives: equalLink, }) diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts index a9bebca3b..fb0577086 100644 --- a/packages/capabilities/src/types.ts +++ b/packages/capabilities/src/types.ts @@ -611,7 +611,7 @@ export interface StoreAddSuccessResult { /** DID of the space this item will be stored in. */ with: DID /** CID of the item. */ - link: UnknownLink + link: CARLink } export interface StoreAddSuccessDone extends StoreAddSuccessResult { @@ -649,7 +649,7 @@ export interface ListResponse { } export interface StoreListItem { - link: UnknownLink + link: CARLink size: number origin?: UnknownLink insertedAt: ISO8601Date diff --git a/packages/capabilities/test/capabilities/store.test.js b/packages/capabilities/test/capabilities/store.test.js index 9295db030..437d1e154 100644 --- a/packages/capabilities/test/capabilities/store.test.js +++ b/packages/capabilities/test/capabilities/store.test.js @@ -13,6 +13,10 @@ import { } from '../helpers/fixtures.js' import { createCarCid, validateAuthorization } from '../helpers/utils.js' +const CAR_LINK = parseLink( + 'bagbaierale63ypabqutmxxbz3qg2yzcp2xhz2yairorogfptwdd5n4lsz5xa' +) + const top = async () => Capability.top.delegate({ issuer: account, @@ -35,7 +39,7 @@ describe('store capabilities', function () { audience: w3, with: account.did(), nb: { - link: parseLink('bafkqaaa'), + link: CAR_LINK, size: 0, }, proofs: [await top()], @@ -55,7 +59,7 @@ describe('store capabilities', function () { assert.deepEqual(result.ok.audience.did(), w3.did()) assert.equal(result.ok.capability.can, 'store/add') assert.deepEqual(result.ok.capability.nb, { - link: parseLink('bafkqaaa'), + link: CAR_LINK, size: 0, }) }) @@ -66,7 +70,7 @@ describe('store capabilities', function () { audience: w3, with: account.did(), nb: { - link: parseLink('bafkqaaa'), + link: CAR_LINK, size: 0, }, proofs: [await store()], @@ -86,7 +90,7 @@ describe('store capabilities', function () { assert.deepEqual(result.ok.audience.did(), w3.did()) assert.equal(result.ok.capability.can, 'store/add') assert.deepEqual(result.ok.capability.nb, { - link: parseLink('bafkqaaa'), + link: CAR_LINK, size: 0, }) }) @@ -104,7 +108,7 @@ describe('store capabilities', function () { audience: w3, with: account.did(), nb: { - link: parseLink('bafkqaaa'), + link: CAR_LINK, size: 0, }, proofs: [store], @@ -124,7 +128,7 @@ describe('store capabilities', function () { assert.deepEqual(result.ok.audience.did(), w3.did()) assert.equal(result.ok.capability.can, 'store/add') assert.deepEqual(result.ok.capability.nb, { - link: parseLink('bafkqaaa'), + link: CAR_LINK, size: 0, }) }) @@ -147,7 +151,7 @@ describe('store capabilities', function () { with: account.did(), nb: { size: 1000, - link: parseLink('bafkqaaa'), + link: CAR_LINK, }, proofs: [delegation], }) @@ -166,7 +170,7 @@ describe('store capabilities', function () { assert.deepEqual(result.ok.audience.did(), w3.did()) assert.equal(result.ok.capability.can, 'store/add') assert.deepEqual(result.ok.capability.nb, { - link: parseLink('bafkqaaa'), + link: CAR_LINK, size: 1000, }) } @@ -178,7 +182,7 @@ describe('store capabilities', function () { with: account.did(), nb: { size: 2048, - link: parseLink('bafkqaaa'), + link: CAR_LINK, }, proofs: [delegation], }) @@ -206,7 +210,7 @@ describe('store capabilities', function () { audience: w3, with: account.did(), nb: { - link: parseLink('bafkqaaa'), + link: CAR_LINK, // @ts-expect-error size, }, @@ -252,7 +256,7 @@ describe('store capabilities', function () { audience: w3, with: account.did(), nb: { - link: parseLink('bafkqaaa'), + link: CAR_LINK, size: 1024.2, }, proofs, diff --git a/packages/upload-api/src/types.ts b/packages/upload-api/src/types.ts index 0b9ba35ae..368fc8f13 100644 --- a/packages/upload-api/src/types.ts +++ b/packages/upload-api/src/types.ts @@ -606,7 +606,7 @@ export type AdminUploadInspectResult = Result< export interface StoreAddInput { space: DID - link: UnknownLink + link: CARLink size: number origin?: UnknownLink issuer: DID diff --git a/packages/upload-api/test/handlers/space-info.spec.js b/packages/upload-api/test/handlers/space-info.spec.js index d2b5b547b..ed22537d1 100644 --- a/packages/upload-api/test/handlers/space-info.spec.js +++ b/packages/upload-api/test/handlers/space-info.spec.js @@ -96,7 +96,9 @@ describe('space/info', function () { proofs: [delegation], nb: { size: 1000, - link: parseLink('bafkqaaa'), + link: parseLink( + 'bagbaierale63ypabqutmxxbz3qg2yzcp2xhz2yairorogfptwdd5n4lsz5xa' + ), }, }), ], @@ -165,7 +167,9 @@ describe('space/info', function () { with: space.did(), proofs: [delegation], nb: { - link: parseLink('bafkqaaa'), + link: parseLink( + 'bagbaierale63ypabqutmxxbz3qg2yzcp2xhz2yairorogfptwdd5n4lsz5xa' + ), }, }), ], diff --git a/packages/upload-api/test/handlers/store.js b/packages/upload-api/test/handlers/store.js index e2619fe63..97fc6ee71 100644 --- a/packages/upload-api/test/handlers/store.js +++ b/packages/upload-api/test/handlers/store.js @@ -2,7 +2,11 @@ import { createServer, connect } from '../../src/lib.js' import * as API from '../../src/types.js' import * as CAR from '@ucanto/transport/car' import { base64pad } from 'multiformats/bases/base64' +import * as Raw from 'multiformats/codecs/raw' +import { sha256 } from 'multiformats/hashes/sha2' +import * as Link from 'multiformats/link' import * as StoreCapabilities from '@web3-storage/capabilities/store' +import { invoke } from '@ucanto/core' import { alice, bob, createSpace, registerSpace } from '../util.js' import { Absentee } from '@ucanto/principal' import { provisionProvider } from '../helpers/utils.js' @@ -449,6 +453,57 @@ export const test = { ) }, + 'store/add fails with non-car link': async (assert, context) => { + const { proof, spaceDid } = await registerSpace(alice, context) + const connection = connect({ + id: context.id, + channel: createServer(context), + }) + + const data = new Uint8Array([11, 22, 34, 44, 55]) + /** @type {API.Link} */ + const link = Link.create(Raw.code, await sha256.digest(data)) + const size = context.maxUploadSize + 1 + + // Throws because invocation builder expects CAR link + try { + StoreCapabilities.add.invoke({ + issuer: alice, + audience: connection.id, + with: spaceDid, + nb: { + link, + size, + }, + proofs: [proof], + }) + assert.ok(false, 'should have throw exception') + } catch (error) { + assert.ok(String(error).match(/0x202 codec/)) + } + + // Going around client validation will still fail because server handler + // expects CAR link. + const invocation = await invoke({ + issuer: alice, + audience: connection.id, + capability: { + can: 'store/add', + with: spaceDid, + nb: { + link, + size, + }, + }, + + proofs: [proof], + }).delegate() + + const [storeAdd] = await connection.execute(invocation) + assert.ok(storeAdd.out.error) + assert.ok(storeAdd.out.error?.message.match('0x202 codec')) + }, + 'store/remove fails for non existent link': async (assert, context) => { const { proof, spaceDid } = await registerSpace(alice, context) const connection = connect({ diff --git a/packages/upload-client/src/store.js b/packages/upload-client/src/store.js index 2e9afa361..db994d880 100644 --- a/packages/upload-client/src/store.js +++ b/packages/upload-client/src/store.js @@ -163,7 +163,7 @@ export async function add( * has the capability to perform the action. * * The issuer needs the `store/get` delegated capability. - * @param {import('multiformats/link').UnknownLink} link CID of stored CAR file. + * @param {import('multiformats/link').Link} link CID of stored CAR file. * @param {import('./types.js').RequestOptions} [options] * @returns {Promise} */ diff --git a/packages/w3up-client/src/capability/store.js b/packages/w3up-client/src/capability/store.js index c1cd72630..92ef9efb8 100644 --- a/packages/w3up-client/src/capability/store.js +++ b/packages/w3up-client/src/capability/store.js @@ -21,7 +21,7 @@ export class StoreClient extends Base { /** * Get details of a stored item. * - * @param {import('../types.js').UnknownLink} link - Root data CID for the DAG that was stored. + * @param {import('../types.js').CARLink} link - Root data CID for the DAG that was stored. * @param {import('../types.js').RequestOptions} [options] */ async get(link, options = {}) {