Skip to content

Commit

Permalink
feat: add store.get and upload.get to clients (#1178)
Browse files Browse the repository at this point in the history
This was implemented in `capabilities` and `upload-api` but for some
reason was never hooked in the clients.
  • Loading branch information
alanshaw committed Nov 25, 2023
1 parent 1010233 commit d1be42a
Show file tree
Hide file tree
Showing 11 changed files with 457 additions and 0 deletions.
57 changes: 57 additions & 0 deletions packages/upload-client/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,63 @@ export async function add(
return link
}

/**
* Get details of a stored item.
*
* Required delegated capability proofs: `store/get`
*
* @param {import('./types.js').InvocationConfig} conf Configuration
* for the UCAN invocation. An object with `issuer`, `with` and `proofs`.
*
* The `issuer` is the signing authority that is issuing the UCAN
* invocation(s). It is typically the user _agent_.
*
* The `with` is the resource the invocation applies to. It is typically the
* DID of a space.
*
* The `proofs` are a set of capability delegations that prove the issuer
* 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('./types.js').RequestOptions} [options]
* @returns {Promise<import('./types.js').StoreGetSuccess>}
*/
export async function get(
{ issuer, with: resource, proofs, audience },
link,
options = {}
) {
/* c8 ignore next */
const conn = options.connection ?? connection
const result = await retry(
async () => {
return await StoreCapabilities.get
.invoke({
issuer,
/* c8 ignore next */
audience: audience ?? servicePrincipal,
with: SpaceDID.from(resource),
nb: { link },
proofs,
})
.execute(conn)
},
{
onFailedAttempt: console.warn,
retries: options.retries ?? REQUEST_RETRIES,
}
)

if (!result.out.ok) {
throw new Error(`failed ${StoreCapabilities.get.can} invocation`, {
cause: result.out.error,
})
}

return result.out.ok
}

/**
* List CAR files stored by the issuer.
*
Expand Down
4 changes: 4 additions & 0 deletions packages/upload-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export type {
StoreAddSuccess,
StoreAddSuccessUpload,
StoreAddSuccessDone,
StoreGetSuccess,
StoreGetFailure,
StoreList,
StoreListSuccess,
StoreListItem,
Expand All @@ -59,6 +61,8 @@ export type {
StoreRemoveFailure,
UploadAdd,
UploadAddSuccess,
UploadGetSuccess,
UploadGetFailure,
UploadList,
UploadListSuccess,
UploadListItem,
Expand Down
57 changes: 57 additions & 0 deletions packages/upload-client/src/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,63 @@ export async function add(
return result.out.ok
}

/**
* Get details of an "upload".
*
* Required delegated capability proofs: `upload/get`
*
* @param {import('./types.js').InvocationConfig} conf Configuration
* for the UCAN invocation. An object with `issuer`, `with` and `proofs`.
*
* The `issuer` is the signing authority that is issuing the UCAN
* invocation(s). It is typically the user _agent_.
*
* The `with` is the resource the invocation applies to. It is typically the
* DID of a space.
*
* The `proofs` are a set of capability delegations that prove the issuer
* has the capability to perform the action.
*
* The issuer needs the `upload/get` delegated capability.
* @param {import('multiformats/link').UnknownLink} root Root data CID for the DAG that was stored.
* @param {import('./types.js').RequestOptions} [options]
* @returns {Promise<import('./types.js').UploadGetSuccess>}
*/
export async function get(
{ issuer, with: resource, proofs, audience },
root,
options = {}
) {
/* c8 ignore next */
const conn = options.connection ?? connection
const result = await retry(
async () => {
return await UploadCapabilities.get
.invoke({
issuer,
/* c8 ignore next */
audience: audience ?? servicePrincipal,
with: SpaceDID.from(resource),
nb: { root },
proofs,
})
.execute(conn)
},
{
onFailedAttempt: console.warn,
retries: options.retries ?? REQUEST_RETRIES,
}
)

if (!result.out.ok) {
throw new Error(`failed ${UploadCapabilities.get.can} invocation`, {
cause: result.out.error,
})
}

return result.out.ok
}

/**
* List uploads created by the issuer.
*
Expand Down
104 changes: 104 additions & 0 deletions packages/upload-client/test/store.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -648,3 +648,107 @@ describe('Store.remove', () => {
)
})
})

describe('Store.get', () => {
it('gets stored item', async () => {
const space = await Signer.generate()
const agent = await Signer.generate()
const car = await randomCAR(128)

const proofs = [
await StoreCapabilities.get.delegate({
issuer: space,
audience: agent,
with: space.did(),
expiration: Infinity,
}),
]

const service = mockService({
store: {
get: provide(StoreCapabilities.get, ({ invocation, capability }) => {
assert.equal(invocation.issuer.did(), agent.did())
assert.equal(invocation.capabilities.length, 1)
assert.equal(capability.can, StoreCapabilities.get.can)
assert.equal(capability.with, space.did())
assert.equal(String(capability.nb?.link), car.cid.toString())
return {
ok: {
link: car.cid,
size: car.size,
insertedAt: new Date().toISOString(),
},
}
}),
},
})

const server = Server.create({
id: serviceSigner,
service,
codec: CAR.inbound,
validateAuthorization,
})
const connection = Client.connect({
id: serviceSigner,
codec: CAR.outbound,
channel: server,
})

const result = await Store.get(
{ issuer: agent, with: space.did(), proofs, audience: serviceSigner },
car.cid,
{ connection }
)

assert(service.store.get.called)
assert.equal(service.store.get.callCount, 1)

assert.equal(result.link.toString(), car.cid.toString())
assert.equal(result.size, car.size)
})

it('throws on service error', async () => {
const space = await Signer.generate()
const agent = await Signer.generate()
const car = await randomCAR(128)

const proofs = [
await StoreCapabilities.get.delegate({
issuer: space,
audience: agent,
with: space.did(),
expiration: Infinity,
}),
]

const service = mockService({
store: {
get: provide(StoreCapabilities.get, () => {
throw new Server.Failure('boom')
}),
},
})

const server = Server.create({
id: serviceSigner,
service,
codec: CAR.inbound,
validateAuthorization,
})
const connection = Client.connect({
id: serviceSigner,
codec: CAR.outbound,
channel: server,
})

await assert.rejects(
Store.get(
{ issuer: agent, with: space.did(), proofs, audience: serviceSigner },
car.cid,
{ connection }
),
{ message: 'failed store/get invocation' }
)
})
})
105 changes: 105 additions & 0 deletions packages/upload-client/test/upload.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,108 @@ describe('Upload.remove', () => {
)
})
})

describe('Upload.get', () => {
it('gets an upload', async () => {
const space = await Signer.generate()
const agent = await Signer.generate()
const car = await randomCAR(128)

const proofs = [
await UploadCapabilities.get.delegate({
issuer: space,
audience: agent,
with: space.did(),
expiration: Infinity,
}),
]

const service = mockService({
upload: {
get: provide(UploadCapabilities.get, ({ invocation, capability }) => {
assert.equal(invocation.issuer.did(), agent.did())
assert.equal(invocation.capabilities.length, 1)
assert.equal(capability.can, UploadCapabilities.get.can)
assert.equal(capability.with, space.did())
assert.equal(String(capability.nb?.root), car.roots[0].toString())
return {
ok: {
root: car.roots[0],
shards: [car.cid],
insertedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
}
}),
},
})

const server = Server.create({
id: serviceSigner,
service,
codec: CAR.inbound,
validateAuthorization,
})
const connection = Client.connect({
id: serviceSigner,
codec: CAR.outbound,
channel: server,
})

const result = await Upload.get(
{ issuer: agent, with: space.did(), proofs, audience: serviceSigner },
car.roots[0],
{ connection }
)

assert(service.upload.get.called)
assert.equal(service.upload.get.callCount, 1)

assert.equal(result.root.toString(), car.roots[0].toString())
assert.equal(result.shards?.[0].toString(), car.cid)
})

it('throws on service error', async () => {
const space = await Signer.generate()
const agent = await Signer.generate()
const car = await randomCAR(128)

const proofs = [
await UploadCapabilities.get.delegate({
issuer: space,
audience: agent,
with: space.did(),
expiration: Infinity,
}),
]

const service = mockService({
upload: {
get: provide(UploadCapabilities.get, () => {
throw new Server.Failure('boom')
}),
},
})

const server = Server.create({
id: serviceSigner,
service,
codec: CAR.inbound,
validateAuthorization,
})
const connection = Client.connect({
id: serviceSigner,
codec: CAR.outbound,
channel: server,
})

await assert.rejects(
Upload.get(
{ issuer: agent, with: space.did(), proofs, audience: serviceSigner },
car.roots[0],
{ connection }
),
{ message: 'failed upload/get invocation' }
)
})
})
12 changes: 12 additions & 0 deletions packages/w3up-client/src/capability/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ export class StoreClient extends Base {
return Store.add(conf, car, options)
}

/**
* 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').RequestOptions} [options]
*/
async get(link, options = {}) {
const conf = await this._invocationConfig([StoreCapabilities.get.can])
options.connection = this._serviceConf.upload
return Store.get(conf, link, options)
}

/**
* List CAR files stored to the resource.
*
Expand Down
Loading

0 comments on commit d1be42a

Please sign in to comment.