diff --git a/packages/capabilities/package.json b/packages/capabilities/package.json index 8cb3085d..a02822f6 100644 --- a/packages/capabilities/package.json +++ b/packages/capabilities/package.json @@ -109,6 +109,7 @@ "uint8arrays": "^5.0.3" }, "devDependencies": { + "@ipld/dag-ucan": "^3.4.0", "@storacha/eslint-config": "workspace:^", "@types/assert": "^1.5.6", "@types/mocha": "^10.0.0", diff --git a/packages/capabilities/src/blob.js b/packages/capabilities/src/blob.js index ea113aab..59269696 100644 --- a/packages/capabilities/src/blob.js +++ b/packages/capabilities/src/blob.js @@ -15,15 +15,7 @@ */ import { capability, Schema, Link, ok } from '@ucanto/validator' import { content } from './space/blob.js' -import { - equalBlob, - equalWith, - SpaceDID, - and, - equal, - checkLink, - Await, -} from './utils.js' +import { equalBlob, equalWith, and, equal, checkLink, Await } from './utils.js' /** * Capability can only be delegated (but not invoked) allowing audience to @@ -50,7 +42,7 @@ export const allocate = capability({ /** Link to the add blob task that initiated the allocation. */ cause: Schema.link({ version: 1 }), /** DID of the user space where the allocation takes place. */ - space: SpaceDID, + space: Schema.bytes(), }), derives: (claimed, delegated) => and(equalWith(claimed, delegated)) || @@ -73,7 +65,7 @@ export const accept = capability({ /** Blob to accept. */ blob: content, /** DID of the user space where allocation took place. */ - space: SpaceDID, + space: Schema.bytes(), /** This task is blocked on `http/put` receipt available */ _put: Await, }), diff --git a/packages/capabilities/test/capabilities/blob.test.js b/packages/capabilities/test/capabilities/blob.test.js index 1cdcb1f3..26cdbb3b 100644 --- a/packages/capabilities/test/capabilities/blob.test.js +++ b/packages/capabilities/test/capabilities/blob.test.js @@ -1,6 +1,7 @@ import assert from 'assert' import { access } from '@ucanto/validator' import { ed25519, Verifier } from '@ucanto/principal' +import * as DID from '@ipld/dag-ucan/did' import * as Blob from '../../src/blob.js' import * as Capability from '../../src/top.js' import { @@ -44,7 +45,7 @@ describe('blob capabilities', function () { digest: car.cid.multihash.bytes, size: car.bytes.length, }, - space: space.did(), + space: DID.parse(space.did()), cause: await createCborCid({ now: Date.now() }), }, proofs: [await top()], @@ -74,7 +75,7 @@ describe('blob capabilities', function () { digest: car.cid.multihash.bytes, size: car.bytes.length, }, - space: space.did(), + space: DID.parse(space.did()), cause: await createCborCid({ now: Date.now() }), }, proofs: [await blob()], @@ -111,7 +112,7 @@ describe('blob capabilities', function () { digest: car.cid.multihash.bytes, size: car.bytes.length, }, - space: space.did(), + space: DID.parse(space.did()), cause: await createCborCid({ now: Date.now() }), }, proofs: [blob], @@ -138,7 +139,7 @@ describe('blob capabilities', function () { audience: bob, with: account.did(), nb: { - space: space0.did(), + space: DID.parse(space0.did()), }, proofs: [await top()], }) @@ -152,7 +153,7 @@ describe('blob capabilities', function () { digest: car.cid.multihash.bytes, size: car.bytes.length, }, - space: space1.did(), + space: DID.parse(space1.did()), cause: await createCborCid({ now: Date.now() }), }, proofs: [blob], @@ -181,7 +182,7 @@ describe('blob capabilities', function () { digest: car.cid.multihash.bytes, size: car.bytes.length, }, - space: space.did(), + space: DID.parse(space.did()), _put: { 'ucan/await': ['.out.ok', await createCborCid('receipt')], }, @@ -213,7 +214,7 @@ describe('blob capabilities', function () { digest: car.cid.multihash.bytes, size: car.bytes.length, }, - space: space.did(), + space: DID.parse(space.did()), _put: { 'ucan/await': ['.out.ok', await createCborCid('receipt')], }, @@ -252,7 +253,7 @@ describe('blob capabilities', function () { digest: car.cid.multihash.bytes, size: car.bytes.length, }, - space: space.did(), + space: DID.parse(space.did()), _put: { 'ucan/await': ['.out.ok', await createCborCid('receipt')], }, @@ -281,7 +282,7 @@ describe('blob capabilities', function () { audience: bob, with: account.did(), nb: { - space: space0.did(), + space: DID.parse(space0.did()), }, proofs: [await top()], }) @@ -295,7 +296,7 @@ describe('blob capabilities', function () { digest: car.cid.multihash.bytes, size: car.bytes.length, }, - space: space1.did(), + space: DID.parse(space1.did()), _put: { 'ucan/await': ['.out.ok', await createCborCid('receipt')], }, diff --git a/packages/upload-api/src/blob/accept.js b/packages/upload-api/src/blob/accept.js index 8f907ff0..b41d5b85 100644 --- a/packages/upload-api/src/blob/accept.js +++ b/packages/upload-api/src/blob/accept.js @@ -3,6 +3,8 @@ import { Message, Invocation } from '@ucanto/core' import * as Transport from '@ucanto/transport/car' import * as API from '../types.js' import * as HTTP from '@storacha/capabilities/http' +import * as DID from '@ipld/dag-ucan/did' +import * as Digest from 'multiformats/hashes/digest' import { AgentMessage } from '../lib.js' /** @@ -95,7 +97,14 @@ export const poll = async (context, receipt) => { return messageWrite } - const register = await context.registry.register(allocate.nb) + const register = await context.registry.register({ + space: /** @type {API.SpaceDID} */ (DID.decode(allocate.nb.space).did()), + cause: allocate.nb.cause, + blob: { + digest: Digest.decode(allocate.nb.blob.digest), + size: allocate.nb.blob.size, + }, + }) if (register.error) { // it's ok if there's already a registration of this blob in this space if (register.error.name !== 'EntryExists') { diff --git a/packages/upload-api/src/blob/add.js b/packages/upload-api/src/blob/add.js index 5d431ef0..5dc3a2cd 100644 --- a/packages/upload-api/src/blob/add.js +++ b/packages/upload-api/src/blob/add.js @@ -6,6 +6,7 @@ import * as Blob from '@storacha/capabilities/blob' import * as SpaceBlob from '@storacha/capabilities/space/blob' import * as HTTP from '@storacha/capabilities/http' import * as Digest from 'multiformats/hashes/digest' +import * as DID from '@ipld/dag-ucan/did' import * as API from '../types.js' import { allocate as spaceAllocate } from '../space-allocate.js' import { createConcludeInvocation } from '../ucan/conclude.js' @@ -126,7 +127,7 @@ async function allocate({ context, blob, space, cause }) { digest: blob.digest, size: blob.size, }, - space, + space: DID.parse(space), cause, }, }) @@ -271,7 +272,7 @@ async function accept({ context, provider, blob, space, delivery }) { with: provider.did(), nb: { blob, - space, + space: DID.parse(space), _put: { 'ucan/await': ['.out.ok', delivery.task.link()] }, }, }) diff --git a/packages/upload-api/src/blob/get.js b/packages/upload-api/src/blob/get.js index 3f3fa998..72d70a05 100644 --- a/packages/upload-api/src/blob/get.js +++ b/packages/upload-api/src/blob/get.js @@ -13,9 +13,20 @@ export function blobGetProvider(context) { const digest = Digest.decode(capability.nb.digest) const space = Server.DID.parse(capability.with).did() const res = await context.registry.find(space, digest) - if (res.error && res.error.name === 'EntryNotFound') { - return Server.error(new BlobNotFound(digest)) + if (res.error) { + if (res.error.name === 'EntryNotFound') { + return Server.error(new BlobNotFound(digest)) + } + return res } - return res + + return Server.ok({ + blob: { + digest: res.ok.blob.digest.bytes, + size: res.ok.blob.size, + }, + cause: res.ok.cause, + insertedAt: res.ok.insertedAt.toISOString(), + }) }) } diff --git a/packages/upload-api/src/blob/list.js b/packages/upload-api/src/blob/list.js index 75614864..5ebaf48d 100644 --- a/packages/upload-api/src/blob/list.js +++ b/packages/upload-api/src/blob/list.js @@ -10,6 +10,20 @@ export function blobListProvider(context) { return Server.provide(SpaceBlob.list, async ({ capability }) => { const space = capability.with const { cursor, size } = capability.nb - return await context.registry.entries(space, { size, cursor }) + const result = await context.registry.entries(space, { size, cursor }) + if (result.error) { + return result + } + return Server.ok({ + ...result.ok, + results: result.ok.results.map((r) => ({ + blob: { + digest: r.blob.digest.bytes, + size: r.blob.size, + }, + cause: r.cause, + insertedAt: r.insertedAt.toISOString(), + })), + }) }) } diff --git a/packages/upload-api/src/types.ts b/packages/upload-api/src/types.ts index 375409eb..18101f19 100644 --- a/packages/upload-api/src/types.ts +++ b/packages/upload-api/src/types.ts @@ -161,7 +161,11 @@ import { RevocationsStorage } from './types/revocations.js' export * from '@storacha/capabilities/types' export * from '@ucanto/interface' -export type { ProvisionsStorage, Provision, Subscription } from './types/provisions.js' +export type { + ProvisionsStorage, + Provision, + Subscription, +} from './types/provisions.js' export type { DelegationsStorage, Query as DelegationsStorageQuery, diff --git a/packages/upload-api/src/types/blob.ts b/packages/upload-api/src/types/blob.ts index b584ba05..ee97d251 100644 --- a/packages/upload-api/src/types/blob.ts +++ b/packages/upload-api/src/types/blob.ts @@ -14,7 +14,6 @@ import type { } from '@ucanto/interface' import { Multihash, - BlobItem as Entry, BlobAllocate, BlobAccept, BlobAllocateSuccess, @@ -24,7 +23,16 @@ import { MultihashDigest } from 'multiformats' import { ListResponse, SpaceDID } from '../types.js' import { Storage } from './storage.js' -export type { Entry } +export interface Blob { + digest: MultihashDigest + size: number +} + +export interface Entry { + blob: Blob + cause: Link + insertedAt: Date +} /** Indicates an entry was not found that matches the passed details. */ export interface EntryNotFound extends Failure { @@ -71,7 +79,7 @@ export interface BlobModel { export interface RegistrationData { space: SpaceDID cause: Link - blob: BlobModel + blob: Blob } export interface BlobService { diff --git a/packages/upload-api/test/external-service/index.js b/packages/upload-api/test/external-service/index.js index 6dedf184..98c1e274 100644 --- a/packages/upload-api/test/external-service/index.js +++ b/packages/upload-api/test/external-service/index.js @@ -5,7 +5,13 @@ import { BrowserStorageNode, StorageNode } from './storage-node.js' import * as BlobRetriever from './blob-retriever.js' import * as RoutingService from './router.js' -export { ClaimsService, BrowserStorageNode, StorageNode, BlobRetriever, RoutingService } +export { + ClaimsService, + BrowserStorageNode, + StorageNode, + BlobRetriever, + RoutingService, +} /** * @param {object} config diff --git a/packages/upload-api/test/external-service/storage-node.js b/packages/upload-api/test/external-service/storage-node.js index 77cb74a0..6c08b4d7 100644 --- a/packages/upload-api/test/external-service/storage-node.js +++ b/packages/upload-api/test/external-service/storage-node.js @@ -10,7 +10,10 @@ import { ed25519 } from '@ucanto/principal' import { CAR, HTTP } from '@ucanto/transport' import * as Server from '@ucanto/server' import { connect } from '@ucanto/client' -import { AllocatedMemoryNotWrittenError, BlobSizeLimitExceededError } from '../../src/blob.js' +import { + AllocatedMemoryNotWrittenError, + BlobSizeLimitExceededError, +} from '../../src/blob.js' /** * @typedef {{ @@ -61,7 +64,12 @@ const createService = ({ const digest = Digest.decode(capability.nb.blob.digest) const checksum = base64pad.baseEncode(digest.digest) if (capability.nb.blob.size > MaxUploadSize) { - return error(new BlobSizeLimitExceededError(capability.nb.blob.size, MaxUploadSize)) + return error( + new BlobSizeLimitExceededError( + capability.nb.blob.size, + MaxUploadSize + ) + ) } if (await contentStore.has(digest)) { return ok({ size: 0 }) @@ -323,7 +331,7 @@ export class StorageNode { /** * @param {API.ClaimsClientContext} ctx - * @param {{ space: API.SpaceDID, digest: API.MultihashDigest, location: API.URI }} params + * @param {{ space: Uint8Array, digest: API.MultihashDigest, location: API.URI }} params */ const publishLocationCommitment = async (ctx, { digest, location }) => { const { invocationConfig, connection } = ctx.claimsService diff --git a/packages/upload-api/test/handlers/blob.js b/packages/upload-api/test/handlers/blob.js index 5ea71ce6..3a491f49 100644 --- a/packages/upload-api/test/handlers/blob.js +++ b/packages/upload-api/test/handlers/blob.js @@ -115,7 +115,7 @@ export const test = { proofs: [proof], // Note: we have to set an expiration, or the default expiration value // will be set when the invocation is executed and the UCAN issued. - expiration: (Math.floor(Date.now() / 1000)) + 10 + expiration: Math.floor(Date.now() / 1000) + 10, }) // Invoke `blob/add` for the first time const firstBlobAdd = await invocation.execute(connection) diff --git a/packages/upload-api/test/storage/blob-registry-tests.js b/packages/upload-api/test/storage/blob-registry-tests.js index 142bba45..13d95efa 100644 --- a/packages/upload-api/test/storage/blob-registry-tests.js +++ b/packages/upload-api/test/storage/blob-registry-tests.js @@ -13,7 +13,7 @@ export const test = { const { registry } = context const data = new Uint8Array([11, 22, 34, 44, 55]) const digest = await sha256.digest(data) - const blob = { digest: digest.bytes, size: data.length } + const blob = { digest: digest, size: data.length } const cause = await randomCID() const registration = await registry.register({ space, blob, cause }) assert.ok(registration.ok) @@ -26,7 +26,7 @@ export const test = { const { registry } = context const data = new Uint8Array([11, 22, 34, 44, 55]) const digest = await sha256.digest(data) - const blob = { digest: digest.bytes, size: data.length } + const blob = { digest: digest, size: data.length } const cause = await randomCID() const registration0 = await registry.register({ space, blob, cause }) assert.ok(registration0.ok) @@ -45,7 +45,7 @@ export const test = { assert.ok(find0.error) assert.equal(find0.error?.name, EntryNotFound.name) - const blob = { digest: digest.bytes, size: data.length } + const blob = { digest: digest, size: data.length } const cause = await randomCID() const registration = await registry.register({ space, blob, cause }) assert.ok(registration.ok) @@ -54,7 +54,9 @@ export const test = { assert.ok(find1.ok) assert.ok(find1.ok?.blob) assert.equal(find1.ok?.blob.size, data.length) - assert.ok(equals(digest.bytes, find1.ok?.blob.digest || new Uint8Array())) + assert.ok( + equals(digest.bytes, find1.ok?.blob.digest.bytes || new Uint8Array()) + ) assert.equal(find1.ok?.cause.toString(), cause.toString()) }, 'should list all blobs in a space': async (assert, context) => { @@ -65,12 +67,12 @@ export const test = { // Data for alice const data0 = new Uint8Array([11, 22, 34, 44, 55]) const digest0 = await sha256.digest(data0) - const blob0 = { digest: digest0.bytes, size: data0.length } + const blob0 = { digest: digest0, size: data0.length } const cause0 = await randomCID() // Data for bob const data1 = new Uint8Array([66, 77, 88, 99, 0]) const digest1 = await sha256.digest(data1) - const blob1 = { digest: digest1.bytes, size: data1.length } + const blob1 = { digest: digest1, size: data1.length } const cause1 = await randomCID() // Get alice empty entries @@ -101,8 +103,8 @@ export const test = { assert.equal(entriesAlice1.ok?.results.length, 1) assert.ok( equals( - blob0.digest, - entriesAlice1.ok?.results[0].blob.digest || new Uint8Array() + blob0.digest.bytes, + entriesAlice1.ok?.results[0].blob.digest.bytes || new Uint8Array() ) ) @@ -121,12 +123,12 @@ export const test = { assert.equal(entriesAlice2.ok?.results.length, 2) assert.ok( entriesAlice2.ok?.results.some((res) => - equals(res.blob.digest, blob0.digest) + equals(res.blob.digest.bytes, blob0.digest.bytes) ) ) assert.ok( entriesAlice2.ok?.results.some((res) => - equals(res.blob.digest, blob1.digest) + equals(res.blob.digest.bytes, blob1.digest.bytes) ) ) }, @@ -144,7 +146,7 @@ export const test = { const { registry } = context const data = new Uint8Array([11, 22, 34, 44, 55]) const digest = await sha256.digest(data) - const blob = { digest: digest.bytes, size: data.length } + const blob = { digest: digest, size: data.length } const cause = await randomCID() const reg = await registry.register({ space, blob, cause }) diff --git a/packages/upload-api/test/storage/blob-registry.js b/packages/upload-api/test/storage/blob-registry.js index 705890d5..a843aea2 100644 --- a/packages/upload-api/test/storage/blob-registry.js +++ b/packages/upload-api/test/storage/blob-registry.js @@ -13,18 +13,17 @@ export class Registry { /** @type {API.BlobAPI.Registry['register']} */ async register({ space, cause, blob }) { const entries = this.data.get(space) ?? [] - if (entries.some((e) => equals(e.blob.digest, blob.digest))) { + if (entries.some((e) => equals(e.blob.digest.bytes, blob.digest.bytes))) { return error(new EntryExists()) } - const insertedAt = new Date().toISOString() - this.data.set(space, [{ blob, cause, insertedAt }, ...entries]) + this.data.set(space, [{ blob, cause, insertedAt: new Date() }, ...entries]) return ok({}) } /** @type {API.BlobAPI.Registry['find']} */ async find(space, digest) { const entries = this.data.get(space) ?? [] - const entry = entries.find((e) => equals(e.blob.digest, digest.bytes)) + const entry = entries.find((e) => equals(e.blob.digest.bytes, digest.bytes)) if (!entry) return error(new EntryNotFound()) return ok(entry) } @@ -32,7 +31,7 @@ export class Registry { /** @type {API.BlobAPI.Registry['deregister']} */ async deregister(space, digest) { const entries = this.data.get(space) ?? [] - const entry = entries.find((e) => equals(e.blob.digest, digest.bytes)) + const entry = entries.find((e) => equals(e.blob.digest.bytes, digest.bytes)) if (!entry) return error(new EntryNotFound()) this.data.set( space, diff --git a/packages/upload-api/test/storage/usage-storage.js b/packages/upload-api/test/storage/usage-storage.js index f364edf6..9c8e5e33 100644 --- a/packages/upload-api/test/storage/usage-storage.js +++ b/packages/upload-api/test/storage/usage-storage.js @@ -53,7 +53,7 @@ export class UsageStorage { return { cause: /** @type {import('../types.js').Link} */ (item.cause), delta: item.size, - receiptAt: item.insertedAt, + receiptAt: item.insertedAt.toISOString(), } }), }, diff --git a/packages/upload-client/test/helpers/utils.js b/packages/upload-client/test/helpers/utils.js index 4194d6ce..5d3dde98 100644 --- a/packages/upload-client/test/helpers/utils.js +++ b/packages/upload-client/test/helpers/utils.js @@ -5,6 +5,7 @@ import { Assert } from '@web3-storage/content-claims/capability' import * as Server from '@ucanto/server' import * as HTTP from '@storacha/capabilities/http' import * as BlobCapabilities from '@storacha/capabilities/blob' +import * as DID from '@ipld/dag-ucan/did' import { createConcludeInvocation } from '../../src/blob/add.js' import { randomCAR } from './random.js' @@ -126,7 +127,7 @@ const setupBlobAddResponse = async function ( nb: { blob, cause: invocation.link(), - space: space.did(), + space: DID.parse(space.did()), }, expiration: Infinity, }) @@ -190,7 +191,7 @@ const setupBlobAddResponse = async function ( with: space.did(), nb: { blob, - space: space.did(), + space: DID.parse(space.did()), _put: { 'ucan/await': ['.out.ok', blobPutTask.link()] }, }, proofs, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c471b10..19f490ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -216,6 +216,9 @@ importers: specifier: ^5.0.3 version: 5.1.0 devDependencies: + '@ipld/dag-ucan': + specifier: ^3.4.0 + version: 3.4.0 '@storacha/eslint-config': specifier: workspace:^ version: link:../eslint-config-w3up