Skip to content
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: add subscription/list capability #1088

Merged
merged 5 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/access-client/src/access.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ export const spaceAccess = {
'upload/*': {},
'access/*': {},
'filecoin/*': {},
'usage/*': {},
}

/**
Expand Down
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 @@ -622,6 +635,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 }

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 @@ -319,6 +329,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: (
customer: AccountDID
) => Promise<Result<SubscriptionListSuccess, SubscriptionListFailure>>
}
49 changes: 49 additions & 0 deletions packages/upload-api/test/handlers/subscription.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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 @@ -21,6 +21,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 @@ -41,6 +42,8 @@ export const createContext = async (
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 dealTrackerSigner = await Signer.generate()
Expand Down Expand Up @@ -69,7 +72,8 @@ export const createContext = async (
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} customer */
async list(customer) {
/** @type {import('../types.js').SubscriptionListItem[]} */
const results = []
const entries = Object.entries(this.provisionsStore.provisions)
for (const [subscription, provision] of entries) {
if (provision.customer !== customer) continue
results.push({
subscription,
provider: provision.provider,
consumers: [provision.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