-
Notifications
You must be signed in to change notification settings - Fork 22
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: blob implementation #1342
Changes from all commits
1a456e8
dece9c4
e09310b
71b4550
05bade8
ab02d08
8931124
9fac96c
730b601
92500c6
c88f634
5a49c72
6579dde
7618768
4de3b96
e58e864
5adcb0d
6f2bf8f
dc5e821
0cc774a
a40e9f2
e5e60d5
f32d7ab
b91dd04
8b0d4ab
4cf99fa
15d732c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/** | ||
* Blob Capabilities. | ||
* | ||
* Blob is a fixed size byte array addressed by the multihash. | ||
* Usually blobs are used to represent set of IPLD blocks at different byte ranges. | ||
* | ||
* These can be imported directly with: | ||
* ```js | ||
* import * as Blob from '@web3-storage/capabilities/blob' | ||
* ``` | ||
* | ||
* @module | ||
*/ | ||
import { capability, Schema } from '@ucanto/validator' | ||
import { equalBlob, equalWith, SpaceDID } from './utils.js' | ||
|
||
/** | ||
* Agent capabilities for Blob protocol | ||
*/ | ||
|
||
/** | ||
* Capability can only be delegated (but not invoked) allowing audience to | ||
* derived any `blob/` prefixed capability for the (memory) space identified | ||
* by DID in the `with` field. | ||
*/ | ||
export const blob = capability({ | ||
can: 'blob/*', | ||
/** | ||
* DID of the (memory) space where Blob is intended to | ||
* be stored. | ||
*/ | ||
with: SpaceDID, | ||
derives: equalWith, | ||
}) | ||
|
||
/** | ||
* Blob description for being ingested by the service. | ||
*/ | ||
export const content = Schema.struct({ | ||
/** | ||
* A multihash digest of the blob payload bytes, uniquely identifying blob. | ||
*/ | ||
digest: Schema.bytes(), | ||
/** | ||
* Number of bytes contained by this blob. Service will provision write target | ||
* for this exact size. Attempt to write a larger Blob file will fail. | ||
*/ | ||
size: Schema.integer(), | ||
}) | ||
|
||
/** | ||
* `blob/add` capability allows agent to store a Blob into a (memory) space | ||
* identified by did:key in the `with` field. Agent should compute blob multihash | ||
* and size and provide it under `nb.blob` field, allowing a service to provision | ||
* a write location for the agent to PUT desired Blob into. | ||
*/ | ||
export const add = capability({ | ||
can: 'blob/add', | ||
/** | ||
* DID of the (memory) space where Blob is intended to | ||
* be stored. | ||
*/ | ||
with: SpaceDID, | ||
nb: Schema.struct({ | ||
/** | ||
* Blob to be added on the space. | ||
*/ | ||
blob: content, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. probably better to not tie the implementation with this, this follows the spec, so I would recommend to discuss this at the spec level via issue/PR rather than here... see storacha/specs#118 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with @vasco-santos on discussing this in spec, however since initial spec was merged I'll respond here. I think we'd like to maintain some flexibility to extend capability over time, e.g. I anticipate that we may want to allow setting Relatedly I'm also exploring thread of making |
||
}), | ||
derives: equalBlob, | ||
}) | ||
|
||
// ⚠️ We export imports here so they are not omitted in generated typedefs | ||
// @see https://github.com/microsoft/TypeScript/issues/51548 | ||
export { Schema } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
/** | ||
* HTTP Capabilities. | ||
* | ||
* These can be imported directly with: | ||
* ```js | ||
* import * as HTTP from '@web3-storage/capabilities/http' | ||
* ``` | ||
* | ||
* @module | ||
*/ | ||
import { capability, Schema, ok } from '@ucanto/validator' | ||
import { content } from './blob.js' | ||
import { equal, equalBody, equalWith, SpaceDID, Await, and } from './utils.js' | ||
|
||
/** | ||
* `http/put` capability invocation MAY be performed by any authorized agent on behalf of the subject | ||
* as long as they have referenced `body` content to do so. | ||
*/ | ||
export const put = capability({ | ||
can: 'http/put', | ||
/** | ||
* DID of the (memory) space where Blob is intended to | ||
* be stored. | ||
*/ | ||
with: SpaceDID, | ||
nb: Schema.struct({ | ||
/** | ||
* Description of body to send (digest/size). | ||
*/ | ||
body: content, | ||
Gozala marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/** | ||
* HTTP(S) location that can receive blob content via HTTP PUT request. | ||
*/ | ||
url: Schema.string().or(Await), | ||
/** | ||
* HTTP headers. | ||
*/ | ||
headers: Schema.dictionary({ value: Schema.string() }).or(Await), | ||
}), | ||
derives: (claim, from) => { | ||
return ( | ||
and(equalWith(claim, from)) || | ||
and(equalBody(claim, from)) || | ||
and(equal(claim.nb.url, from.nb, 'url')) || | ||
and(equal(claim.nb.headers, from.nb, 'headers')) || | ||
ok({}) | ||
) | ||
}, | ||
}) |
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -21,6 +21,9 @@ import { | |||||||||||||||||
import { space, info } from './space.js' | ||||||||||||||||||
import * as provider from './provider.js' | ||||||||||||||||||
import { top } from './top.js' | ||||||||||||||||||
import * as BlobCaps from './blob.js' | ||||||||||||||||||
import * as W3sBlobCaps from './web3.storage/blob.js' | ||||||||||||||||||
import * as HTTPCaps from './http.js' | ||||||||||||||||||
import * as StoreCaps from './store.js' | ||||||||||||||||||
import * as UploadCaps from './upload.js' | ||||||||||||||||||
import * as AccessCaps from './access.js' | ||||||||||||||||||
|
@@ -41,6 +44,10 @@ export type ISO8601Date = string | |||||||||||||||||
|
||||||||||||||||||
export type { Unit, PieceLink } | ||||||||||||||||||
|
||||||||||||||||||
export interface UCANAwait<Selector extends string = string, Task = unknown> { | ||||||||||||||||||
'ucan/await': [Selector, Link<Task>] | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
/** | ||||||||||||||||||
* An IPLD Link that has the CAR codec code. | ||||||||||||||||||
*/ | ||||||||||||||||||
|
@@ -439,6 +446,95 @@ export interface UploadNotFound extends Ucanto.Failure { | |||||||||||||||||
|
||||||||||||||||||
export type UploadGetFailure = UploadNotFound | Ucanto.Failure | ||||||||||||||||||
|
||||||||||||||||||
// HTTP | ||||||||||||||||||
export type HTTPPut = InferInvokedCapability<typeof HTTPCaps.put> | ||||||||||||||||||
|
||||||||||||||||||
// Blob | ||||||||||||||||||
export type Blob = InferInvokedCapability<typeof BlobCaps.blob> | ||||||||||||||||||
export type BlobAdd = InferInvokedCapability<typeof BlobCaps.add> | ||||||||||||||||||
export type ServiceBlob = InferInvokedCapability<typeof W3sBlobCaps.blob> | ||||||||||||||||||
export type BlobAllocate = InferInvokedCapability<typeof W3sBlobCaps.allocate> | ||||||||||||||||||
export type BlobAccept = InferInvokedCapability<typeof W3sBlobCaps.accept> | ||||||||||||||||||
|
||||||||||||||||||
export type BlobMultihash = Uint8Array | ||||||||||||||||||
export interface BlobModel { | ||||||||||||||||||
digest: BlobMultihash | ||||||||||||||||||
size: number | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// Blob add | ||||||||||||||||||
export interface BlobAddSuccess { | ||||||||||||||||||
site: UCANAwait<'.out.ok.site'> | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
export interface BlobSizeOutsideOfSupportedRange extends Ucanto.Failure { | ||||||||||||||||||
name: 'BlobSizeOutsideOfSupportedRange' | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
export interface AwaitError extends Ucanto.Failure { | ||||||||||||||||||
name: 'AwaitError' | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// TODO: We need Ucanto.Failure because provideAdvanced can't handle errors without it | ||||||||||||||||||
export type BlobAddFailure = | ||||||||||||||||||
| BlobSizeOutsideOfSupportedRange | ||||||||||||||||||
| AwaitError | ||||||||||||||||||
| StorageGetError | ||||||||||||||||||
| Ucanto.Failure | ||||||||||||||||||
|
||||||||||||||||||
export interface BlobListItem { | ||||||||||||||||||
blob: BlobModel | ||||||||||||||||||
insertedAt: ISO8601Date | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// Blob allocate | ||||||||||||||||||
export interface BlobAllocateSuccess { | ||||||||||||||||||
size: number | ||||||||||||||||||
address?: BlobAddress | ||||||||||||||||||
} | ||||||||||||||||||
Comment on lines
+491
to
+494
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would expect
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Reading this PR got me wondering if perhaps we should have used There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we please align this on the spec https://github.com/web3-storage/specs/blob/fix/align-blob-with-impl/w3-blob.md#allocate-blob ? It is difficult to manage what is expected if we are always diverging from there. For a second invocation with different nonce, I think not creating a presigned url for something is there makes sense, but happy to reconsider There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I already raised this a week ago #1342 (comment) and gets difficult to make progress this way :/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||
|
||||||||||||||||||
export interface BlobAddress { | ||||||||||||||||||
url: ToString<URL> | ||||||||||||||||||
headers: Record<string, string> | ||||||||||||||||||
expiresAt: ISO8601Date | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest we call this
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also isn't ttl already present via There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We talked about not making this provider specific yesterday and the limitations that aws gives. see #1342 (comment) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to have a date here, otherwise I cannot infer if presigned URL is expired, and therefore allocation needs to run again. TTL is usually how long more available, rather then a date. So, we would be doing the same issue that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per wikipedia about TTL
I mostly have seen timestamps in TTL but in datagram it is indeed more of a lifetime left, so I think you are correct. Relatedly wiki mentions Expires header and same named cookie so I think you are correct that TTL may not be a good name. How about renaming it to In regards to my comment of not replicating data already in headers, my primary motivation here is to avoid getting two out of sync, but after thinking more about it I think you are correct. We may choose to expire it sooner than what's in the presigned URL so perhaps having two out of sync is not so bad, although having our expiry higher than what's in headers still bad, but I think it's ok. |
||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// If user space has not enough space to allocate the blob. | ||||||||||||||||||
export interface NotEnoughStorageCapacity extends Ucanto.Failure { | ||||||||||||||||||
name: 'NotEnoughStorageCapacity' | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
export type BlobAllocateFailure = NotEnoughStorageCapacity | Ucanto.Failure | ||||||||||||||||||
|
||||||||||||||||||
// Blob accept | ||||||||||||||||||
export interface BlobAcceptSuccess { | ||||||||||||||||||
// A Link for a delegation with site commiment for the added blob. | ||||||||||||||||||
site: Link | ||||||||||||||||||
vasco-santos marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
export interface AllocatedMemoryHadNotBeenWrittenTo extends Ucanto.Failure { | ||||||||||||||||||
name: 'AllocatedMemoryHadNotBeenWrittenTo' | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// TODO: We should type the store errors and add them here, instead of Ucanto.Failure | ||||||||||||||||||
export type BlobAcceptFailure = | ||||||||||||||||||
| AllocatedMemoryHadNotBeenWrittenTo | ||||||||||||||||||
| Ucanto.Failure | ||||||||||||||||||
|
||||||||||||||||||
// Storage errors | ||||||||||||||||||
export type StoragePutError = StorageOperationError | ||||||||||||||||||
export type StorageGetError = StorageOperationError | RecordNotFound | ||||||||||||||||||
|
||||||||||||||||||
// Operation on a storage failed with unexpected error | ||||||||||||||||||
export interface StorageOperationError extends Error { | ||||||||||||||||||
name: 'StorageOperationFailed' | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// Record requested not found in the storage | ||||||||||||||||||
export interface RecordNotFound extends Error { | ||||||||||||||||||
name: 'RecordNotFound' | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// Store | ||||||||||||||||||
export type Store = InferInvokedCapability<typeof StoreCaps.store> | ||||||||||||||||||
export type StoreAdd = InferInvokedCapability<typeof StoreCaps.add> | ||||||||||||||||||
|
@@ -530,6 +626,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 { | ||||||||||||||||||
/** | ||||||||||||||||||
|
@@ -540,6 +637,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. | ||||||||||||||||||
|
@@ -578,6 +677,15 @@ export type UCANRevokeFailure = | |||||||||||||||||
| UnauthorizedRevocation | ||||||||||||||||||
| RevocationsStoreFailure | ||||||||||||||||||
|
||||||||||||||||||
/** | ||||||||||||||||||
* Error is raised when receipt is received for unknown invocation | ||||||||||||||||||
*/ | ||||||||||||||||||
export interface ReferencedInvocationNotFound extends Ucanto.Failure { | ||||||||||||||||||
name: 'ReferencedInvocationNotFound' | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
export type UCANConcludeFailure = ReferencedInvocationNotFound | Ucanto.Failure | ||||||||||||||||||
|
||||||||||||||||||
// Admin | ||||||||||||||||||
export type Admin = InferInvokedCapability<typeof AdminCaps.admin> | ||||||||||||||||||
export type AdminUploadInspect = InferInvokedCapability< | ||||||||||||||||||
|
@@ -686,6 +794,7 @@ export type ServiceAbilityArray = [ | |||||||||||||||||
Access['can'], | ||||||||||||||||||
AccessAuthorize['can'], | ||||||||||||||||||
UCANAttest['can'], | ||||||||||||||||||
UCANConclude['can'], | ||||||||||||||||||
CustomerGet['can'], | ||||||||||||||||||
ConsumerHas['can'], | ||||||||||||||||||
ConsumerGet['can'], | ||||||||||||||||||
|
@@ -708,7 +817,13 @@ export type ServiceAbilityArray = [ | |||||||||||||||||
AdminStoreInspect['can'], | ||||||||||||||||||
PlanGet['can'], | ||||||||||||||||||
Usage['can'], | ||||||||||||||||||
UsageReport['can'] | ||||||||||||||||||
UsageReport['can'], | ||||||||||||||||||
Blob['can'], | ||||||||||||||||||
BlobAdd['can'], | ||||||||||||||||||
ServiceBlob['can'], | ||||||||||||||||||
BlobAllocate['can'], | ||||||||||||||||||
BlobAccept['can'], | ||||||||||||||||||
HTTPPut['can'] | ||||||||||||||||||
] | ||||||||||||||||||
|
||||||||||||||||||
/** | ||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably expose other capabilities also
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
they are exposes because are in top level https://github.com/web3-storage/w3up/blob/feat/blob-implementation/packages/capabilities/package.json#L32