diff --git a/packages/upload-client/src/blob.js b/packages/upload-client/src/blob.js index e62a567a5..fd4960921 100644 --- a/packages/upload-client/src/blob.js +++ b/packages/upload-client/src/blob.js @@ -280,16 +280,18 @@ export async function add( } // Invoke `conclude` with `http/put` receipt - const derivedSigner = ed25519.from( - /** @type {import('@ucanto/interface').SignerArchive} */ - (nextTasks.put.task.facts[0]['keys']) - ) - - const httpPutReceipt = await Receipt.issue({ - issuer: derivedSigner, - ran: nextTasks.put.task.cid, - result: { ok: {} }, - }) + let { receipt: httpPutReceipt } = nextTasks.put + if (!httpPutReceipt?.out.ok) { + const derivedSigner = ed25519.from( + /** @type {import('@ucanto/interface').SignerArchive} */ + (nextTasks.put.task.facts[0]['keys']) + ) + httpPutReceipt = await Receipt.issue({ + issuer: derivedSigner, + ran: nextTasks.put.task.cid, + result: { ok: {} }, + }) + } const httpPutConcludeInvocation = createConcludeInvocation( issuer, // @ts-expect-error object of type unknown @@ -297,7 +299,6 @@ export async function add( httpPutReceipt ) const ucanConclude = await httpPutConcludeInvocation.execute(conn) - if (!ucanConclude.out.ok) { throw new Error(`failed ${BlobCapabilities.add.can} invocation`, { cause: result.out.error, diff --git a/packages/upload-client/test/blob.test.js b/packages/upload-client/test/blob.test.js index 88ada0c2a..71ec04eae 100644 --- a/packages/upload-client/test/blob.test.js +++ b/packages/upload-client/test/blob.test.js @@ -17,6 +17,7 @@ import { setupBlobAdd4xxResponse, setupBlobAdd5xxResponse, setupBlobAddWithAcceptReceiptSuccessResponse, + setupBlobAddWithHttpPutReceiptSuccessResponse, receiptsEndpoint, } from './helpers/utils.js' import { fetchWithUploadProgress } from '../src/fetch-with-upload-progress.js' @@ -374,6 +375,71 @@ describe('Blob.add', () => { assert.ok(site.capabilities[0].nb.content.multihash.bytes) }) + it('reuses the http/put receipt when it is already available', async () => { + const space = await Signer.generate() + const agent = await Signer.generate() + const bytes = await randomBytes(128) + const bytesHash = await sha256.digest(bytes) + + const proofs = [ + await BlobCapabilities.add.delegate({ + issuer: space, + audience: agent, + with: space.did(), + expiration: Infinity, + }), + ] + + const service = mockService({ + ucan: { + conclude: provide(UCAN.conclude, () => { + return { ok: { time: Date.now() } } + }), + }, + space: { + blob: { + // @ts-ignore Argument of type + add: provide(BlobCapabilities.add, ({ invocation }) => { + return setupBlobAddWithHttpPutReceiptSuccessResponse( + { issuer: space, audience: agent, with: space, proofs }, + invocation + ) + }), + }, + }, + }) + + const server = Server.create({ + id: serviceSigner, + service, + codec: CAR.inbound, + validateAuthorization, + }) + const connection = Client.connect({ + id: serviceSigner, + codec: CAR.outbound, + channel: server, + }) + + const { site, multihash } = await Blob.add( + { issuer: agent, with: space.did(), proofs, audience: serviceSigner }, + bytes, + { + connection, + receiptsEndpoint, + } + ) + + assert(multihash) + assert.deepEqual(multihash, bytesHash) + + assert(site) + assert.equal(site.capabilities[0].can, Assert.location.can) + // we're not verifying this as it's a mocked value + // @ts-ignore nb unknown + assert.ok(site.capabilities[0].nb.content.multihash.bytes) + }) + it('throws for bucket URL client error 4xx', async () => { const space = await Signer.generate() const agent = await Signer.generate() diff --git a/packages/upload-client/test/helpers/utils.js b/packages/upload-client/test/helpers/utils.js index e64891bb7..b35f0a171 100644 --- a/packages/upload-client/test/helpers/utils.js +++ b/packages/upload-client/test/helpers/utils.js @@ -23,6 +23,7 @@ export const setupBlobAddSuccessResponse = async function ( 'http://localhost:9200', options, invocation, + false, false ) } @@ -37,6 +38,7 @@ export const setupBlobAdd4xxResponse = async function ( 'http://localhost:9400', options, invocation, + false, false ) } @@ -51,6 +53,7 @@ export const setupBlobAdd5xxResponse = async function ( 'http://localhost:9500', options, invocation, + false, false ) } @@ -65,12 +68,29 @@ export const setupBlobAddWithAcceptReceiptSuccessResponse = async function ( 'http://localhost:9200', options, invocation, + false, true ) } +export const setupBlobAddWithHttpPutReceiptSuccessResponse = async function ( + // @ts-ignore + options, + // @ts-ignore + invocation +) { + return setupBlobAddResponse( + 'http://localhost:9200', + options, + invocation, + true, + false + ) +} + /** * @param {string} url + * @param {boolean} hasHttpPutReceipt * @param {boolean} hasAcceptReceipt */ const setupBlobAddResponse = async function ( @@ -79,6 +99,7 @@ const setupBlobAddResponse = async function ( { issuer, with: space, proofs, audience }, // @ts-ignore invocation, + hasHttpPutReceipt, hasAcceptReceipt ) { const blob = invocation.capabilities[0].nb.blob @@ -134,6 +155,18 @@ const setupBlobAddResponse = async function ( expiration: Infinity, }) .delegate() + const blobPutReceipt = !hasHttpPutReceipt + ? await Receipt.issue({ + issuer, + ran: blobPutTask.cid, + result: { error: new Error() }, + }) + : await generateAcceptReceipt(blobPutTask.cid.toString()) + const blobConcludePut = await createConcludeInvocation( + issuer, + audience, + blobPutReceipt + ).delegate() const blobAcceptTask = await W3sBlobCapabilities.accept .invoke({ @@ -149,13 +182,14 @@ const setupBlobAddResponse = async function ( }) .delegate() - const blobAcceptReceipt = hasAcceptReceipt + // FIXME not generating the right kind of receipt here, but it should be enough for mocking + const blobAcceptReceipt = !hasAcceptReceipt ? await Receipt.issue({ issuer, ran: blobAcceptTask.cid, result: { error: new Error() }, }) - : await generateAcceptReceipt(invocation.cid.toString()) + : await generateAcceptReceipt(blobAcceptTask.cid.toString()) const blobConcludeAccept = await createConcludeInvocation( issuer, audience, @@ -170,6 +204,7 @@ const setupBlobAddResponse = async function ( .fork(blobAllocateTask) .fork(blobConcludeAllocate) .fork(blobPutTask) + .fork(blobConcludePut) .fork(blobAcceptTask) .fork(blobConcludeAccept) }