Skip to content

Commit

Permalink
feat: add capability
Browse files Browse the repository at this point in the history
  • Loading branch information
Alan Shaw committed Nov 7, 2023
1 parent 6418b4b commit 7f740f9
Show file tree
Hide file tree
Showing 24 changed files with 221 additions and 15 deletions.
1 change: 1 addition & 0 deletions packages/access-client/src/agent-use-cases.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export async function authorizeAndWait(access, email, opts = {}) {
{ can: 'space/*' },
{ can: 'store/*' },
{ can: 'provider/add' },
{ can: 'subscription/list' },
{ can: 'upload/*' },
{ can: 'ucan/*' },
{ can: 'plan/*' },
Expand Down
10 changes: 10 additions & 0 deletions packages/access-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ import type {
PlanGet,
PlanGetSuccess,
PlanGetFailure,
SubscriptionList,
SubscriptionListSuccess,
SubscriptionListFailure,
} from '@web3-storage/capabilities/types'
import type { SetRequired } from 'type-fest'
import { Driver } from './drivers/types.js'
Expand Down Expand Up @@ -116,6 +119,13 @@ export interface Service {
space: {
info: ServiceMethod<SpaceInfo, SpaceInfoResult, Failure | SpaceUnknown>
}
subscription: {
list: ServiceMethod<
SubscriptionList,
SubscriptionListSuccess,
SubscriptionListFailure
>
}
ucan: {
revoke: ServiceMethod<UCANRevoke, UCANRevokeSuccess, UCANRevokeFailure>
}
Expand Down
4 changes: 1 addition & 3 deletions packages/capabilities/src/customer.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { capability, DID, struct, ok } from '@ucanto/validator'
import { equalWith, and, equal } from './utils.js'
import { AccountDID, equalWith, and, equal } from './utils.js'

// e.g. did:web:web3.storage or did:web:staging.web3.storage
export const ProviderDID = DID.match({ method: 'web' })

export const AccountDID = DID.match({ method: 'mailto' })

/**
* Capability can be invoked by a provider to get information about the
* customer.
Expand Down
1 change: 1 addition & 0 deletions packages/capabilities/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const abilitiesAsStrings = [
Consumer.has.can,
Consumer.get.can,
Subscription.get.can,
Subscription.list.can,
RateLimit.add.can,
RateLimit.remove.can,
RateLimit.list.can,
Expand Down
6 changes: 2 additions & 4 deletions packages/capabilities/src/plan.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { capability, DID, ok } from '@ucanto/validator'
import { equalWith, and } from './utils.js'

export const AccountDID = DID.match({ method: 'mailto' })
import { capability, ok } from '@ucanto/validator'
import { AccountDID, equalWith, and } from './utils.js'

/**
* Capability can be invoked by an account to get information about
Expand Down
4 changes: 2 additions & 2 deletions packages/capabilities/src/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
* @module
*/
import { capability, DID, struct, ok } from '@ucanto/validator'
import { equalWith, and, equal, SpaceDID } from './utils.js'
import { AccountDID, equalWith, and, equal, SpaceDID } from './utils.js'

// e.g. did:web:web3.storage or did:web:staging.web3.storage
export const Provider = DID.match({ method: 'web' })

export const AccountDID = DID.match({ method: 'mailto' })
export { AccountDID }

/**
* Capability can be invoked by an agent to add a provider to a space.
Expand Down
12 changes: 11 additions & 1 deletion packages/capabilities/src/subscription.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { capability, DID, struct, ok, Schema } from '@ucanto/validator'
import { equalWith, and, equal } from './utils.js'
import { AccountDID, equalWith, and, equal } from './utils.js'

// e.g. did:web:web3.storage or did:web:staging.web3.storage
export const ProviderDID = DID.match({ method: 'web' })
Expand All @@ -21,3 +21,13 @@ export const get = capability({
)
},
})

/**
* Capability can be invoked to retrieve the list of subscriptions for an
* account.
*/
export const list = capability({
can: 'subscription/list',
with: AccountDID,
derives: equalWith,
})
14 changes: 14 additions & 0 deletions packages/capabilities/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,19 @@ export type SubscriptionGetFailure =
| UnknownProvider
| Ucanto.Failure

export type SubscriptionList = InferInvokedCapability<
typeof SubscriptionCaps.list
>
export interface SubscriptionListSuccess {
results: Array<SubscriptionListItem>
}
export interface SubscriptionListItem {
subscription: string
provider: ProviderDID
consumers: SpaceDID[]
}
export type SubscriptionListFailure = Ucanto.Failure

// Rate Limit
export type RateLimitAdd = InferInvokedCapability<typeof RateLimitCaps.add>
export interface RateLimitAddSuccess {
Expand Down Expand Up @@ -604,6 +617,7 @@ export type AbilitiesArray = [
ConsumerHas['can'],
ConsumerGet['can'],
SubscriptionGet['can'],
SubscriptionList['can'],
RateLimitAdd['can'],
RateLimitRemove['can'],
RateLimitList['can'],
Expand Down
2 changes: 2 additions & 0 deletions packages/capabilities/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const ProviderDID = DID.match({ method: 'web' })

export const SpaceDID = DID.match({ method: 'key' })

export const AccountDID = DID.match({ method: 'mailto' })

/**
* Check URI can be delegated
*
Expand Down
2 changes: 2 additions & 0 deletions packages/upload-api/src/subscription.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as Types from './types.js'
import * as Get from './subscription/get.js'
import * as List from './subscription/list.js'

/**
* @param {Types.SubscriptionServiceContext} context
*/
export const createService = (context) => ({
get: Get.provide(context),
list: List.provide(context),
})
17 changes: 17 additions & 0 deletions packages/upload-api/src/subscription/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as API from '../types.js'
import * as Server from '@ucanto/server'
import { Subscription } from '@web3-storage/capabilities'

/**
* @param {API.SubscriptionServiceContext} context
*/
export const provide = (context) =>
Server.provide(Subscription.list, (input) => list(input, context))

/**
* @param {API.Input<Subscription.list>} input
* @param {API.SubscriptionServiceContext} context
* @returns {Promise<API.Result<API.SubscriptionListSuccess, API.SubscriptionListFailure>>}
*/
const list = async ({ capability }, context) =>
context.subscriptionsStorage.list(capability.with)
11 changes: 11 additions & 0 deletions packages/upload-api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ import {
SubscriptionGet,
SubscriptionGetSuccess,
SubscriptionGetFailure,
SubscriptionList,
SubscriptionListSuccess,
SubscriptionListFailure,
RateLimitAdd,
RateLimitAddSuccess,
RateLimitAddFailure,
Expand Down Expand Up @@ -152,6 +155,8 @@ export type {
export type { RateLimitsStorage, RateLimit } from './types/rate-limits.js'
import { PlansStorage } from './types/plans.js'
export type { PlansStorage } from './types/plans.js'
import { SubscriptionsStorage } from './types/subscriptions.js'
export type { SubscriptionsStorage } from './types/subscriptions.js'

export interface Service extends StorefrontService {
store: {
Expand Down Expand Up @@ -209,6 +214,11 @@ export interface Service extends StorefrontService {
SubscriptionGetSuccess,
SubscriptionGetFailure
>
list: ServiceMethod<
SubscriptionList,
SubscriptionListSuccess,
SubscriptionListFailure
>
}
'rate-limit': {
add: ServiceMethod<RateLimitAdd, RateLimitAddSuccess, RateLimitAddFailure>
Expand Down Expand Up @@ -317,6 +327,7 @@ export interface ProviderServiceContext {
export interface SubscriptionServiceContext {
signer: EdSigner.Signer
provisionsStorage: Provisions
subscriptionsStorage: SubscriptionsStorage
}

export interface RateLimitServiceContext {
Expand Down
12 changes: 12 additions & 0 deletions packages/upload-api/src/types/subscriptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Result } from '@ucanto/interface'
import {
AccountDID,
SubscriptionListSuccess,
SubscriptionListFailure
} from '@web3-storage/capabilities/types'

export interface SubscriptionsStorage {
list: (
account: AccountDID
) => Promise<Result<SubscriptionListSuccess, SubscriptionListFailure>>
}
43 changes: 43 additions & 0 deletions packages/upload-api/test/handlers/subscription.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Subscription } from '@web3-storage/capabilities'
import * as API from '../../src/types.js'
import { createServer, connect } from '../../src/lib.js'
import { alice, registerSpace } from '../util.js'
import { createAuthorization } from '../helpers/utils.js'

/** @type {API.Tests} */
export const test = {
'subscription/list retrieves subscriptions for account': async (assert, context) => {
const spaces = await Promise.all([
registerSpace(alice, context, 'alic_e'),
registerSpace(alice, context, 'alic_e')
])
const connection = connect({
id: context.id,
channel: createServer(context),
})

const subListRes = await Subscription.list
.invoke({
issuer: alice,
audience: context.id,
with: spaces[0].account.did(),
nb: {},
proofs: await createAuthorization({
agent: alice,
account: spaces[0].account,
service: context.service,
}),
})
.execute(connection)

assert.ok(subListRes.out.ok)

const results = subListRes.out.ok?.results
const totalConsumers = results?.reduce((total, s) => total + s.consumers.length, 0)
assert.equal(totalConsumers, spaces.length)

for (const space of spaces) {
assert.ok(results?.some(s => s.consumers[0] === space.spaceDid))
}
},
}
3 changes: 3 additions & 0 deletions packages/upload-api/test/handlers/subscription.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as Subscription from './subscription.js'
import { test } from '../test.js'
test({ 'subscription/*': Subscription.test })
1 change: 0 additions & 1 deletion packages/upload-api/test/handlers/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export const test = {
/** @type {import('../types.js').ProviderDID} */
(context.id.did())
const report = usageReportRes.out.ok?.[provider]
console.log(report)
assert.equal(report?.space, spaceDid)
assert.equal(report?.size.initial, 0)
assert.equal(report?.size.final, size)
Expand Down
6 changes: 5 additions & 1 deletion packages/upload-api/test/helpers/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import * as TestTypes from '../types.js'
import { confirmConfirmationUrl } from './utils.js'
import { PlansStorage } from '../storage/plans-storage.js'
import { UsageStorage } from '../storage/usage-storage.js'
import { SubscriptionsStorage } from '../storage/subscriptions-storage.js'

/**
* @param {object} options
Expand All @@ -35,6 +36,8 @@ export const createContext = async (options = {}) => {
const revocationsStorage = new RevocationsStorage()
const plansStorage = new PlansStorage()
const usageStorage = new UsageStorage(storeTable)
const provisionsStorage = new ProvisionsStorage(options.providers)
const subscriptionsStorage = new SubscriptionsStorage(provisionsStorage)
const signer = await Signer.generate()
const aggregatorSigner = await Signer.generate()
const id = signer.withDID('did:web:test.web3.storage')
Expand All @@ -56,7 +59,8 @@ export const createContext = async (options = {}) => {
signer: id,
email,
url: new URL('http://localhost:8787'),
provisionsStorage: new ProvisionsStorage(options.providers),
provisionsStorage,
subscriptionsStorage,
delegationsStorage: new DelegationsStorage(),
rateLimitsStorage: new RateLimitsStorage(),
plansStorage,
Expand Down
2 changes: 2 additions & 0 deletions packages/upload-api/test/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as RateLimitAdd from './handlers/rate-limit/add.js'
import * as RateLimitList from './handlers/rate-limit/list.js'
import * as RateLimitRemove from './handlers/rate-limit/remove.js'
import * as Store from './handlers/store.js'
import * as Subscription from './handlers/subscription.js'
import * as Upload from './handlers/upload.js'
import * as Plan from './handlers/plan.js'
import * as Usage from './handlers/usage.js'
Expand Down Expand Up @@ -43,6 +44,7 @@ export const handlerTests = {
...RateLimitList,
...RateLimitRemove,
...Store.test,
...Subscription.test,
...Upload.test,
...Plan.test,
...Usage.test,
Expand Down
29 changes: 29 additions & 0 deletions packages/upload-api/test/storage/subscriptions-storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @typedef {import('../../src/types/subscriptions.js').SubscriptionsStorage} SubscriptionsStore
*/

/**
* @implements {SubscriptionsStore}
*/
export class SubscriptionsStorage {
/** @param {import('./provisions-storage.js').ProvisionsStorage} provisions */
constructor (provisions) {
this.provisionsStore = provisions
}

/** @param {import('../types.js').AccountDID} account */
async list(account) {
/** @type {import('../types.js').SubscriptionListItem[]} */
const results = []
const entries = Object.entries(this.provisionsStore.provisions)
for (const [subscription, { customer, provider, consumer }] of entries) {
if (customer !== account) continue
results.push({
subscription,
provider,
consumers: [consumer]
})
}
return { ok: { results } }
}
}
4 changes: 2 additions & 2 deletions packages/upload-api/test/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ export async function createSpace(audience) {
}

/**
*
* @param {API.Principal & API.Signer} audience
* @param {import('./types.js').UcantoServerTestContext} context
* @param {string} [username]
*/
export const registerSpace = async (audience, context, username = 'alice') => {
const { proof, space, spaceDid } = await createSpace(audience)
Expand All @@ -77,7 +77,7 @@ export const registerSpace = async (audience, context, username = 'alice') => {
})
}

return { proof, space, spaceDid }
return { proof, space, spaceDid, account }
}

/** @param {number} size */
Expand Down
8 changes: 8 additions & 0 deletions packages/w3up-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,18 @@
"types": "./dist/src/capability/store.d.ts",
"import": "./src/capability/store.js"
},
"./capability/subscription": {
"types": "./dist/src/capability/subscription.d.ts",
"import": "./src/capability/subscription.js"
},
"./capability/upload": {
"types": "./dist/src/capability/upload.d.ts",
"import": "./src/capability/upload.js"
},
"./capability/usage": {
"types": "./dist/src/capability/usage.d.ts",
"import": "./src/capability/usage.js"
},
"./types": "./src/types.js"
},
"publishConfig": {
Expand Down
Loading

0 comments on commit 7f740f9

Please sign in to comment.