Skip to content

Commit 00735a8

Browse files
authored
feat: blob, web3.storage and ucan conclude capabilities together with api handlers (#1342)
Adds implementation of `blob/*`, `web3.storage/*` and `ucan/conclude` handlers and capabilities.
1 parent 232dadd commit 00735a8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+3903
-49
lines changed

packages/capabilities/package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@
5656
"types": "./dist/src/filecoin/dealer.d.ts",
5757
"import": "./src/filecoin/dealer.js"
5858
},
59+
"./web3.storage/blob": {
60+
"types": "./dist/src/web3.storage/blob.d.ts",
61+
"import": "./src/web3.storage/blob.js"
62+
},
5963
"./types": {
6064
"types": "./dist/src/types.d.ts",
6165
"import": "./src/types.js"
@@ -88,7 +92,8 @@
8892
"@ucanto/principal": "^9.0.1",
8993
"@ucanto/transport": "^9.1.1",
9094
"@ucanto/validator": "^9.0.2",
91-
"@web3-storage/data-segment": "^3.2.0"
95+
"@web3-storage/data-segment": "^3.2.0",
96+
"uint8arrays": "^5.0.3"
9297
},
9398
"devDependencies": {
9499
"@web3-storage/eslint-config-w3up": "workspace:^",

packages/capabilities/src/blob.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Blob Capabilities.
3+
*
4+
* Blob is a fixed size byte array addressed by the multihash.
5+
* Usually blobs are used to represent set of IPLD blocks at different byte ranges.
6+
*
7+
* These can be imported directly with:
8+
* ```js
9+
* import * as Blob from '@web3-storage/capabilities/blob'
10+
* ```
11+
*
12+
* @module
13+
*/
14+
import { capability, Schema } from '@ucanto/validator'
15+
import { equalBlob, equalWith, SpaceDID } from './utils.js'
16+
17+
/**
18+
* Agent capabilities for Blob protocol
19+
*/
20+
21+
/**
22+
* Capability can only be delegated (but not invoked) allowing audience to
23+
* derived any `blob/` prefixed capability for the (memory) space identified
24+
* by DID in the `with` field.
25+
*/
26+
export const blob = capability({
27+
can: 'blob/*',
28+
/**
29+
* DID of the (memory) space where Blob is intended to
30+
* be stored.
31+
*/
32+
with: SpaceDID,
33+
derives: equalWith,
34+
})
35+
36+
/**
37+
* Blob description for being ingested by the service.
38+
*/
39+
export const content = Schema.struct({
40+
/**
41+
* A multihash digest of the blob payload bytes, uniquely identifying blob.
42+
*/
43+
digest: Schema.bytes(),
44+
/**
45+
* Number of bytes contained by this blob. Service will provision write target
46+
* for this exact size. Attempt to write a larger Blob file will fail.
47+
*/
48+
size: Schema.integer(),
49+
})
50+
51+
/**
52+
* `blob/add` capability allows agent to store a Blob into a (memory) space
53+
* identified by did:key in the `with` field. Agent should compute blob multihash
54+
* and size and provide it under `nb.blob` field, allowing a service to provision
55+
* a write location for the agent to PUT desired Blob into.
56+
*/
57+
export const add = capability({
58+
can: 'blob/add',
59+
/**
60+
* DID of the (memory) space where Blob is intended to
61+
* be stored.
62+
*/
63+
with: SpaceDID,
64+
nb: Schema.struct({
65+
/**
66+
* Blob to be added on the space.
67+
*/
68+
blob: content,
69+
}),
70+
derives: equalBlob,
71+
})
72+
73+
// ⚠️ We export imports here so they are not omitted in generated typedefs
74+
// @see https://github.com/microsoft/TypeScript/issues/51548
75+
export { Schema }

packages/capabilities/src/http.js

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* HTTP Capabilities.
3+
*
4+
* These can be imported directly with:
5+
* ```js
6+
* import * as HTTP from '@web3-storage/capabilities/http'
7+
* ```
8+
*
9+
* @module
10+
*/
11+
import { capability, Schema, ok } from '@ucanto/validator'
12+
import { content } from './blob.js'
13+
import { equal, equalBody, equalWith, SpaceDID, Await, and } from './utils.js'
14+
15+
/**
16+
* `http/put` capability invocation MAY be performed by any authorized agent on behalf of the subject
17+
* as long as they have referenced `body` content to do so.
18+
*/
19+
export const put = capability({
20+
can: 'http/put',
21+
/**
22+
* DID of the (memory) space where Blob is intended to
23+
* be stored.
24+
*/
25+
with: SpaceDID,
26+
nb: Schema.struct({
27+
/**
28+
* Description of body to send (digest/size).
29+
*/
30+
body: content,
31+
/**
32+
* HTTP(S) location that can receive blob content via HTTP PUT request.
33+
*/
34+
url: Schema.string().or(Await),
35+
/**
36+
* HTTP headers.
37+
*/
38+
headers: Schema.dictionary({ value: Schema.string() }).or(Await),
39+
}),
40+
derives: (claim, from) => {
41+
return (
42+
and(equalWith(claim, from)) ||
43+
and(equalBody(claim, from)) ||
44+
and(equal(claim.nb.url, from.nb, 'url')) ||
45+
and(equal(claim.nb.headers, from.nb, 'headers')) ||
46+
ok({})
47+
)
48+
},
49+
})

packages/capabilities/src/index.js

+10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import * as DealTracker from './filecoin/deal-tracker.js'
1919
import * as UCAN from './ucan.js'
2020
import * as Plan from './plan.js'
2121
import * as Usage from './usage.js'
22+
import * as Blob from './blob.js'
23+
import * as W3sBlob from './web3.storage/blob.js'
24+
import * as HTTP from './http.js'
2225

2326
export {
2427
Access,
@@ -63,6 +66,7 @@ export const abilitiesAsStrings = [
6366
Access.access.can,
6467
Access.authorize.can,
6568
UCAN.attest.can,
69+
UCAN.conclude.can,
6670
Customer.get.can,
6771
Consumer.has.can,
6872
Consumer.get.can,
@@ -86,4 +90,10 @@ export const abilitiesAsStrings = [
8690
Plan.get.can,
8791
Usage.usage.can,
8892
Usage.report.can,
93+
Blob.blob.can,
94+
Blob.add.can,
95+
W3sBlob.blob.can,
96+
W3sBlob.allocate.can,
97+
W3sBlob.accept.can,
98+
HTTP.put.can,
8999
]

packages/capabilities/src/types.ts

+116-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import {
2121
import { space, info } from './space.js'
2222
import * as provider from './provider.js'
2323
import { top } from './top.js'
24+
import * as BlobCaps from './blob.js'
25+
import * as W3sBlobCaps from './web3.storage/blob.js'
26+
import * as HTTPCaps from './http.js'
2427
import * as StoreCaps from './store.js'
2528
import * as UploadCaps from './upload.js'
2629
import * as AccessCaps from './access.js'
@@ -41,6 +44,10 @@ export type ISO8601Date = string
4144

4245
export type { Unit, PieceLink }
4346

47+
export interface UCANAwait<Selector extends string = string, Task = unknown> {
48+
'ucan/await': [Selector, Link<Task>]
49+
}
50+
4451
/**
4552
* An IPLD Link that has the CAR codec code.
4653
*/
@@ -439,6 +446,95 @@ export interface UploadNotFound extends Ucanto.Failure {
439446

440447
export type UploadGetFailure = UploadNotFound | Ucanto.Failure
441448

449+
// HTTP
450+
export type HTTPPut = InferInvokedCapability<typeof HTTPCaps.put>
451+
452+
// Blob
453+
export type Blob = InferInvokedCapability<typeof BlobCaps.blob>
454+
export type BlobAdd = InferInvokedCapability<typeof BlobCaps.add>
455+
export type ServiceBlob = InferInvokedCapability<typeof W3sBlobCaps.blob>
456+
export type BlobAllocate = InferInvokedCapability<typeof W3sBlobCaps.allocate>
457+
export type BlobAccept = InferInvokedCapability<typeof W3sBlobCaps.accept>
458+
459+
export type BlobMultihash = Uint8Array
460+
export interface BlobModel {
461+
digest: BlobMultihash
462+
size: number
463+
}
464+
465+
// Blob add
466+
export interface BlobAddSuccess {
467+
site: UCANAwait<'.out.ok.site'>
468+
}
469+
470+
export interface BlobSizeOutsideOfSupportedRange extends Ucanto.Failure {
471+
name: 'BlobSizeOutsideOfSupportedRange'
472+
}
473+
474+
export interface AwaitError extends Ucanto.Failure {
475+
name: 'AwaitError'
476+
}
477+
478+
// TODO: We need Ucanto.Failure because provideAdvanced can't handle errors without it
479+
export type BlobAddFailure =
480+
| BlobSizeOutsideOfSupportedRange
481+
| AwaitError
482+
| StorageGetError
483+
| Ucanto.Failure
484+
485+
export interface BlobListItem {
486+
blob: BlobModel
487+
insertedAt: ISO8601Date
488+
}
489+
490+
// Blob allocate
491+
export interface BlobAllocateSuccess {
492+
size: number
493+
address?: BlobAddress
494+
}
495+
496+
export interface BlobAddress {
497+
url: ToString<URL>
498+
headers: Record<string, string>
499+
expiresAt: ISO8601Date
500+
}
501+
502+
// If user space has not enough space to allocate the blob.
503+
export interface NotEnoughStorageCapacity extends Ucanto.Failure {
504+
name: 'NotEnoughStorageCapacity'
505+
}
506+
507+
export type BlobAllocateFailure = NotEnoughStorageCapacity | Ucanto.Failure
508+
509+
// Blob accept
510+
export interface BlobAcceptSuccess {
511+
// A Link for a delegation with site commiment for the added blob.
512+
site: Link
513+
}
514+
515+
export interface AllocatedMemoryHadNotBeenWrittenTo extends Ucanto.Failure {
516+
name: 'AllocatedMemoryHadNotBeenWrittenTo'
517+
}
518+
519+
// TODO: We should type the store errors and add them here, instead of Ucanto.Failure
520+
export type BlobAcceptFailure =
521+
| AllocatedMemoryHadNotBeenWrittenTo
522+
| Ucanto.Failure
523+
524+
// Storage errors
525+
export type StoragePutError = StorageOperationError
526+
export type StorageGetError = StorageOperationError | RecordNotFound
527+
528+
// Operation on a storage failed with unexpected error
529+
export interface StorageOperationError extends Error {
530+
name: 'StorageOperationFailed'
531+
}
532+
533+
// Record requested not found in the storage
534+
export interface RecordNotFound extends Error {
535+
name: 'RecordNotFound'
536+
}
537+
442538
// Store
443539
export type Store = InferInvokedCapability<typeof StoreCaps.store>
444540
export type StoreAdd = InferInvokedCapability<typeof StoreCaps.add>
@@ -530,6 +626,7 @@ export interface UploadListSuccess extends ListResponse<UploadListItem> {}
530626

531627
export type UCANRevoke = InferInvokedCapability<typeof UCANCaps.revoke>
532628
export type UCANAttest = InferInvokedCapability<typeof UCANCaps.attest>
629+
export type UCANConclude = InferInvokedCapability<typeof UCANCaps.conclude>
533630

534631
export interface Timestamp {
535632
/**
@@ -540,6 +637,8 @@ export interface Timestamp {
540637

541638
export type UCANRevokeSuccess = Timestamp
542639

640+
export type UCANConcludeSuccess = Timestamp
641+
543642
/**
544643
* Error is raised when `UCAN` being revoked is not supplied or it's proof chain
545644
* leading to supplied `scope` is not supplied.
@@ -578,6 +677,15 @@ export type UCANRevokeFailure =
578677
| UnauthorizedRevocation
579678
| RevocationsStoreFailure
580679

680+
/**
681+
* Error is raised when receipt is received for unknown invocation
682+
*/
683+
export interface ReferencedInvocationNotFound extends Ucanto.Failure {
684+
name: 'ReferencedInvocationNotFound'
685+
}
686+
687+
export type UCANConcludeFailure = ReferencedInvocationNotFound | Ucanto.Failure
688+
581689
// Admin
582690
export type Admin = InferInvokedCapability<typeof AdminCaps.admin>
583691
export type AdminUploadInspect = InferInvokedCapability<
@@ -686,6 +794,7 @@ export type ServiceAbilityArray = [
686794
Access['can'],
687795
AccessAuthorize['can'],
688796
UCANAttest['can'],
797+
UCANConclude['can'],
689798
CustomerGet['can'],
690799
ConsumerHas['can'],
691800
ConsumerGet['can'],
@@ -708,7 +817,13 @@ export type ServiceAbilityArray = [
708817
AdminStoreInspect['can'],
709818
PlanGet['can'],
710819
Usage['can'],
711-
UsageReport['can']
820+
UsageReport['can'],
821+
Blob['can'],
822+
BlobAdd['can'],
823+
ServiceBlob['can'],
824+
BlobAllocate['can'],
825+
BlobAccept['can'],
826+
HTTPPut['can']
712827
]
713828

714829
/**

packages/capabilities/src/ucan.js

+29-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* UCAN core capabilities.
33
*/
44

5-
import { capability, Schema } from '@ucanto/validator'
5+
import { capability, Schema, ok } from '@ucanto/validator'
66
import * as API from '@ucanto/interface'
77
import { equalWith, equal, and, checkLink } from './utils.js'
88

@@ -74,6 +74,34 @@ export const revoke = capability({
7474
),
7575
})
7676

77+
/**
78+
* `ucan/conclude` capability represents a receipt using a special UCAN capability.
79+
*
80+
* The UCAN invocation specification defines receipt record, that is cryptographically
81+
* signed description of the invocation output and requested effects. Receipt
82+
* structure is very similar to UCAN except it has no notion of expiry nor it is
83+
* possible to delegate ability to issue receipt to another principal.
84+
*/
85+
export const conclude = capability({
86+
can: 'ucan/conclude',
87+
/**
88+
* DID of the principal representing the Conclusion Authority.
89+
* MUST be the DID of the audience of the ran invocation.
90+
*/
91+
with: Schema.did(),
92+
nb: Schema.struct({
93+
/**
94+
* CID of the content with the Receipt.
95+
*/
96+
receipt: Schema.link(),
97+
}),
98+
derives: (claim, from) =>
99+
// With field MUST be the same
100+
and(equalWith(claim, from)) ||
101+
and(checkLink(claim.nb.receipt, from.nb.receipt, 'nb.receipt')) ||
102+
ok({}),
103+
})
104+
77105
/**
78106
* Issued by trusted authority (usually the one handling invocation) that attest
79107
* that specific UCAN delegation has been considered authentic.

0 commit comments

Comments
 (0)