Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: restrict store API to CARs #1415

Merged
merged 1 commit into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
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
11 changes: 8 additions & 3 deletions packages/capabilities/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -94,7 +99,7 @@ export const get = capability({
/**
* shard CID to fetch info about.
*/
link: Link.optional(),
link: CARLink.optional(),
}),
derives: equalLink,
})
Expand All @@ -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,
})
Expand Down
4 changes: 2 additions & 2 deletions packages/capabilities/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -649,7 +649,7 @@ export interface ListResponse<R> {
}

export interface StoreListItem {
link: UnknownLink
link: CARLink
size: number
origin?: UnknownLink
insertedAt: ISO8601Date
Expand Down
26 changes: 15 additions & 11 deletions packages/capabilities/test/capabilities/store.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()],
Expand All @@ -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,
})
})
Expand All @@ -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()],
Expand All @@ -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,
})
})
Expand All @@ -104,7 +108,7 @@ describe('store capabilities', function () {
audience: w3,
with: account.did(),
nb: {
link: parseLink('bafkqaaa'),
link: CAR_LINK,
size: 0,
},
proofs: [store],
Expand All @@ -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,
})
})
Expand All @@ -147,7 +151,7 @@ describe('store capabilities', function () {
with: account.did(),
nb: {
size: 1000,
link: parseLink('bafkqaaa'),
link: CAR_LINK,
},
proofs: [delegation],
})
Expand All @@ -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,
})
}
Expand All @@ -178,7 +182,7 @@ describe('store capabilities', function () {
with: account.did(),
nb: {
size: 2048,
link: parseLink('bafkqaaa'),
link: CAR_LINK,
},
proofs: [delegation],
})
Expand Down Expand Up @@ -206,7 +210,7 @@ describe('store capabilities', function () {
audience: w3,
with: account.did(),
nb: {
link: parseLink('bafkqaaa'),
link: CAR_LINK,
// @ts-expect-error
size,
},
Expand Down Expand Up @@ -252,7 +256,7 @@ describe('store capabilities', function () {
audience: w3,
with: account.did(),
nb: {
link: parseLink('bafkqaaa'),
link: CAR_LINK,
size: 1024.2,
},
proofs,
Expand Down
2 changes: 1 addition & 1 deletion packages/upload-api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ export type AdminUploadInspectResult = Result<

export interface StoreAddInput {
space: DID
link: UnknownLink
link: CARLink
size: number
origin?: UnknownLink
issuer: DID
Expand Down
8 changes: 6 additions & 2 deletions packages/upload-api/test/handlers/space-info.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ describe('space/info', function () {
proofs: [delegation],
nb: {
size: 1000,
link: parseLink('bafkqaaa'),
link: parseLink(
'bagbaierale63ypabqutmxxbz3qg2yzcp2xhz2yairorogfptwdd5n4lsz5xa'
),
},
}),
],
Expand Down Expand Up @@ -165,7 +167,9 @@ describe('space/info', function () {
with: space.did(),
proofs: [delegation],
nb: {
link: parseLink('bafkqaaa'),
link: parseLink(
'bagbaierale63ypabqutmxxbz3qg2yzcp2xhz2yairorogfptwdd5n4lsz5xa'
),
},
}),
],
Expand Down
55 changes: 55 additions & 0 deletions packages/upload-api/test/handlers/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<unknown, any>} */
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({
Expand Down
2 changes: 1 addition & 1 deletion packages/upload-client/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown, CAR.codec.code>} link CID of stored CAR file.
* @param {import('./types.js').RequestOptions} [options]
* @returns {Promise<import('./types.js').StoreGetSuccess>}
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/w3up-client/src/capability/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}) {
Expand Down
Loading