Skip to content

Commit

Permalink
feat: http put
Browse files Browse the repository at this point in the history
  • Loading branch information
vasco-santos committed Apr 3, 2024
1 parent 099c37a commit a78c3ea
Show file tree
Hide file tree
Showing 30 changed files with 1,065 additions and 622 deletions.
41 changes: 41 additions & 0 deletions packages/capabilities/src/blob.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,47 @@ export const allocate = capability({
},
})

/**
* `http/put` capability invocation MAY be performed by any agent on behalf of the subject.
* The `blob/add` provider MUST add `/http/put` effect and capture private key of the
* `subject` in the `meta` field so that any agent could perform it.
*/
export const put = capability({
can: 'http/put',
/**
* DID of the (memory) space where Blob is intended to
* be stored.
*/
with: SpaceDID,
nb: Schema.struct({
/**
* A multihash digest of the blob payload bytes, uniquely identifying blob.
*/
content: Schema.bytes(),
/**
* Blob to accept.
*/
address: Schema.struct({
/**
* HTTP(S) location that can receive blob content via HTTP PUT request.
*/
url: Schema.string(),
/**
* HTTP headers.
*/
headers: Schema.unknown(),
}).optional(),
}),
derives: (claim, from) => {
return (
and(equalContent(claim, from)) ||
and(equal(claim.nb.address?.url, from.nb.address, 'url')) ||
and(equal(claim.nb.address?.headers, from.nb.address, 'headers')) ||
ok({})
)
},
})

/**
* `blob/accept` capability invocation should either succeed when content is
* delivered on allocated address or fail if no content is allocation expires
Expand Down
2 changes: 2 additions & 0 deletions packages/capabilities/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const abilitiesAsStrings = [
Access.access.can,
Access.authorize.can,
UCAN.attest.can,
UCAN.conclude.can,
Customer.get.can,
Consumer.has.can,
Consumer.get.can,
Expand Down Expand Up @@ -92,6 +93,7 @@ export const abilitiesAsStrings = [
Blob.remove.can,
Blob.list.can,
Blob.serviceBlob.can,
Blob.put.can,
Blob.allocate.can,
Blob.accept.can,
]
12 changes: 12 additions & 0 deletions packages/capabilities/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ export type BlobAdd = InferInvokedCapability<typeof BlobCaps.add>
export type BlobRemove = InferInvokedCapability<typeof BlobCaps.remove>
export type BlobList = InferInvokedCapability<typeof BlobCaps.list>
export type ServiceBlob = InferInvokedCapability<typeof BlobCaps.serviceBlob>
export type BlobPut = InferInvokedCapability<typeof BlobCaps.put>
export type BlobAllocate = InferInvokedCapability<typeof BlobCaps.allocate>
export type BlobAccept = InferInvokedCapability<typeof BlobCaps.accept>

Expand Down Expand Up @@ -605,6 +606,7 @@ export interface UploadListSuccess extends ListResponse<UploadListItem> {}

export type UCANRevoke = InferInvokedCapability<typeof UCANCaps.revoke>
export type UCANAttest = InferInvokedCapability<typeof UCANCaps.attest>
export type UCANConclude = InferInvokedCapability<typeof UCANCaps.conclude>

export interface Timestamp {
/**
Expand All @@ -615,6 +617,8 @@ export interface Timestamp {

export type UCANRevokeSuccess = Timestamp

export type UCANConcludeSuccess = Timestamp

/**
* Error is raised when `UCAN` being revoked is not supplied or it's proof chain
* leading to supplied `scope` is not supplied.
Expand Down Expand Up @@ -653,6 +657,12 @@ export type UCANRevokeFailure =
| UnauthorizedRevocation
| RevocationsStoreFailure

export interface InvocationNotFound extends Ucanto.Failure {
name: 'InvocationNotFound'
}

export type UCANConcludeFailure = InvocationNotFound | Ucanto.Failure

// Admin
export type Admin = InferInvokedCapability<typeof AdminCaps.admin>
export type AdminUploadInspect = InferInvokedCapability<
Expand Down Expand Up @@ -761,6 +771,7 @@ export type ServiceAbilityArray = [
Access['can'],
AccessAuthorize['can'],
UCANAttest['can'],
UCANConclude['can'],
CustomerGet['can'],
ConsumerHas['can'],
ConsumerGet['can'],
Expand Down Expand Up @@ -789,6 +800,7 @@ export type ServiceAbilityArray = [
BlobRemove['can'],
BlobList['can'],
ServiceBlob['can'],
BlobPut['can'],
BlobAllocate['can'],
BlobAccept['can']
]
Expand Down
53 changes: 53 additions & 0 deletions packages/capabilities/src/ucan.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,59 @@ export const revoke = capability({
),
})

/**
* `ucan/conclude` capability represents a receipt using a special UCAN capability.
*
* The UCAN invocation specification defines receipt record, that is cryptographically
* signed description of the invocation output and requested effects. Receipt
* structure is very similar to UCAN except it has no notion of expiry nor it is
* possible to delegate ability to issue receipt to another principal.
*/
export const conclude = capability({
can: 'ucan/conclude',
/**
* DID of the principal representing the Conclusion Authority.
* MUST be the DID of the audience of the ran invocation.
*/
with: Schema.did(),
// TODO: Should this just have bytes?
nb: Schema.struct({
/**
* A link to the UCAN invocation that this receipt is for.
*/
ran: UCANLink,
/**
* The value output of the invocation in Result format.
*/
out: Schema.unknown(),
/**
* Tasks that the invocation would like to enqueue.
*/
next: Schema.array(UCANLink),
/**
* Additional data about the receipt
*/
meta: Schema.unknown(),
/**
* The UTC Unix timestamp at which the Receipt was issued
*/
time: Schema.integer(),
}),
derives: (claim, from) =>
// With field MUST be the same
and(equalWith(claim, from)) ??
// invocation MUST be the same
and(checkLink(claim.nb.ran, from.nb.ran, 'nb.ran')) ??
// value output MUST be the same
and(equal(claim.nb.out, from.nb.out, 'nb.out')) ??
// tasks to enqueue MUST be the same
and(equal(claim.nb.next, from.nb.next, 'nb.next')) ??
// additional data MUST be the same
and(equal(claim.nb.meta, from.nb.meta, 'nb.meta')) ??
// the receipt issue time MUST be the same
equal(claim.nb.time, from.nb.time, 'nb.time'),
})

/**
* Issued by trusted authority (usually the one handling invocation) that attest
* that specific UCAN delegation has been considered authentic.
Expand Down
2 changes: 1 addition & 1 deletion packages/capabilities/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export const equalBlob = (claimed, delegated) => {
}

/**
* @template {Types.ParsedCapability<"blob/add"|"blob/remove"|"blob/allocate"|"blob/accept", Types.URI<'did:'>, {content: Uint8Array}>} T
* @template {Types.ParsedCapability<"blob/add"|"blob/remove"|"blob/allocate"|"blob/accept"|"http/put", Types.URI<'did:'>, {content: Uint8Array}>} T
* @param {T} claimed
* @param {T} delegated
* @returns {Types.Result<{}, Types.Failure>}
Expand Down
4 changes: 3 additions & 1 deletion packages/filecoin-api/src/storefront/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ export interface ClaimsClientContext {
*/
claimsService: {
invocationConfig: ClaimsInvocationConfig
connection: ConnectionView<import('@web3-storage/content-claims/server/service/api').Service>
connection: ConnectionView<
import('@web3-storage/content-claims/server/service/api').Service
>
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/filecoin-api/test/context/mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export function mockService(impl) {
info: withCallParams(impl.deal?.info ?? notImplemented),
},
assert: {
equals: withCallParams(impl.assert?.equals ?? notImplemented)
}
equals: withCallParams(impl.assert?.equals ?? notImplemented),
},
}
}

Expand Down
13 changes: 8 additions & 5 deletions packages/filecoin-api/test/context/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,15 @@ export function getMockService() {
}),
},
assert: {
equals: Server.provide(Assert.equals, async ({ capability, invocation }) => {
return {
ok: {}
equals: Server.provide(
Assert.equals,
async ({ capability, invocation }) => {
return {
ok: {},
}
}
})
}
),
},
})
}

Expand Down
50 changes: 27 additions & 23 deletions packages/filecoin-api/test/events/storefront.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as StorefrontEvents from '../../src/storefront/events.js'
import {
StoreOperationErrorName,
UnexpectedStateErrorName,
BlobNotFoundErrorName
BlobNotFoundErrorName,
} from '../../src/errors.js'

import { randomCargo, randomAggregate } from '../utils.js'
Expand Down Expand Up @@ -53,23 +53,24 @@ export const test = {
assert.ok(hasStoredPiece.ok)
assert.equal(hasStoredPiece.ok?.status, 'submitted')
},
'handles filecoin submit messages with error if blob of content is not stored': async (assert, context) => {
// Generate piece for test
const [cargo] = await randomCargo(1, 128)

// Store piece into store
const message = {
piece: cargo.link.link(),
content: cargo.content.link(),
group: context.id.did(),
}
'handles filecoin submit messages with error if blob of content is not stored':
async (assert, context) => {
// Generate piece for test
const [cargo] = await randomCargo(1, 128)

// Store piece into store
const message = {
piece: cargo.link.link(),
content: cargo.content.link(),
group: context.id.did(),
}

// Handle message
const handledMessageRes =
await StorefrontEvents.handleFilecoinSubmitMessage(context, message)
assert.ok(handledMessageRes.error)
assert.equal(handledMessageRes.error?.name, BlobNotFoundErrorName)
},
// Handle message
const handledMessageRes =
await StorefrontEvents.handleFilecoinSubmitMessage(context, message)
assert.ok(handledMessageRes.error)
assert.equal(handledMessageRes.error?.name, BlobNotFoundErrorName)
},
'handles filecoin submit messages deduping when stored': async (
assert,
context
Expand Down Expand Up @@ -255,7 +256,10 @@ export const test = {
)
)
},
'handles piece insert event to issue equivalency claims successfully': async (assert, context) => {
'handles piece insert event to issue equivalency claims successfully': async (
assert,
context
) => {
// Generate piece for test
const [cargo] = await randomCargo(1, 128)

Expand All @@ -274,10 +278,11 @@ export const test = {
}

// Handle message
const handledMessageRes = await StorefrontEvents.handlePieceInsertToEquivalencyClaim(
context,
pieceRecord
)
const handledMessageRes =
await StorefrontEvents.handlePieceInsertToEquivalencyClaim(
context,
pieceRecord
)
assert.ok(handledMessageRes.ok)
// Verify invocation
// @ts-expect-error not typed hooks
Expand All @@ -294,7 +299,6 @@ export const test = {
context.service.assert?.equals?._params[0].nb.equals
)
)

},
'handles piece status update event successfully': async (assert, context) => {
// Generate piece for test
Expand Down
4 changes: 3 additions & 1 deletion packages/filecoin-api/test/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export interface StorefrontTestEventsContext
piece: Partial<import('../src/types.js').AggregatorService['piece']>
aggregate: Partial<import('../src/types.js').DealerService['aggregate']>
deal: Partial<import('../src/types.js').DealTrackerService['deal']>
assert: Partial<import('@web3-storage/content-claims/server/service/api').AssertService>
assert: Partial<
import('@web3-storage/content-claims/server/service/api').AssertService
>
}>
}
2 changes: 1 addition & 1 deletion packages/upload-api/src/blob/accept.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function blobAcceptProvider(context) {
return Server.provide(Blob.accept, async ({ capability }) => {
const { blob } = capability.nb
// If blob is not stored, we must fail
const hasBlob = await context.blobStorage.has(blob.content)
const hasBlob = await context.blobsStorage.has(blob.content)
if (hasBlob.error) {
return {
error: new BlobItemNotFound(),
Expand Down
Loading

0 comments on commit a78c3ea

Please sign in to comment.