From 1ccbfe9f84ae5b2e99e315c92d15d2b54e9723ba Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Tue, 16 Jan 2024 16:50:10 -0800 Subject: [PATCH] feat: change `plan/update` to `plan/set` and use existing `PlansStorage#set` to implement an invocation handler (#1258) We already have a method on the `PlansStorage` interface that has the semantics we want - rename `plan/update` to `plan/set` and add a handler implementation that hands off to the `PlanStorage#set` method. --- packages/capabilities/src/plan.js | 4 +- packages/capabilities/src/types.ts | 6 +-- .../test/capabilities/plan.test.js | 44 +++++++++---------- packages/upload-api/src/plan/set.js | 18 ++++++++ packages/upload-api/src/types/plans.ts | 7 +-- 5 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 packages/upload-api/src/plan/set.js diff --git a/packages/capabilities/src/plan.js b/packages/capabilities/src/plan.js index 10c078fa6..4b3b5b260 100644 --- a/packages/capabilities/src/plan.js +++ b/packages/capabilities/src/plan.js @@ -16,8 +16,8 @@ export const get = capability({ /** * Capability can be invoked by an account to change its billing plan. */ -export const update = capability({ - can: 'plan/update', +export const set = capability({ + can: 'plan/set', with: AccountDID, nb: struct({ product: DID, diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts index 38bbaaa22..f1eb89800 100644 --- a/packages/capabilities/src/types.ts +++ b/packages/capabilities/src/types.ts @@ -630,9 +630,9 @@ export interface PlanNotFound extends Ucanto.Failure { export type PlanGetFailure = PlanNotFound -export type PlanUpdate = InferInvokedCapability +export type PlanSet = InferInvokedCapability -export type PlanUpdateSuccess = Unit +export type PlanSetSuccess = Unit export interface AccountNotFound extends Ucanto.Failure { name: 'AccountNotFound' @@ -642,7 +642,7 @@ export interface InvalidPlanName extends Ucanto.Failure { name: 'InvalidPlanName' } -export type PlanUpdateFailure = AccountNotFound +export type PlanSetFailure = AccountNotFound // Top export type Top = InferInvokedCapability diff --git a/packages/capabilities/test/capabilities/plan.test.js b/packages/capabilities/test/capabilities/plan.test.js index f8d5fe838..83312888d 100644 --- a/packages/capabilities/test/capabilities/plan.test.js +++ b/packages/capabilities/test/capabilities/plan.test.js @@ -95,11 +95,11 @@ describe('plan/get', function () { }) }) -describe('plan/update', function () { +describe('plan/set', function () { const agent = alice const account = 'did:mailto:mallory.com:mallory' it('can invoke as an account', async function () { - const auth = Plan.update.invoke({ + const auth = Plan.set.invoke({ issuer: agent, audience: service, with: account, @@ -109,7 +109,7 @@ describe('plan/update', function () { proofs: await createAuthorization({ agent, service, account }), }) const result = await access(await auth.delegate(), { - capability: Plan.update, + capability: Plan.set, principal: Verifier, authority: service, validateAuthorization, @@ -118,14 +118,14 @@ describe('plan/update', function () { assert.fail(`error in self issue: ${result.error.message}`) } else { assert.deepEqual(result.ok.audience.did(), service.did()) - assert.equal(result.ok.capability.can, 'plan/update') + assert.equal(result.ok.capability.can, 'plan/set') assert.deepEqual(result.ok.capability.with, account) } }) it('fails without account delegation', async function () { const agent = alice - const auth = Plan.update.invoke({ + const auth = Plan.set.invoke({ issuer: agent, audience: service, with: account, @@ -135,7 +135,7 @@ describe('plan/update', function () { }) const result = await access(await auth.delegate(), { - capability: Plan.update, + capability: Plan.set, principal: Verifier, authority: service, validateAuthorization, @@ -145,7 +145,7 @@ describe('plan/update', function () { }) it('fails when invoked by a different agent', async function () { - const auth = Plan.update.invoke({ + const auth = Plan.set.invoke({ issuer: bob, audience: service, with: account, @@ -156,7 +156,7 @@ describe('plan/update', function () { }) const result = await access(await auth.delegate(), { - capability: Plan.update, + capability: Plan.set, principal: Verifier, authority: service, validateAuthorization, @@ -164,8 +164,8 @@ describe('plan/update', function () { assert.equal(result.error?.message.includes('not authorized'), true) }) - it('can delegate plan/update', async function () { - const invocation = Plan.update.invoke({ + it('can delegate plan/set', async function () { + const invocation = Plan.set.invoke({ issuer: bob, audience: service, with: account, @@ -173,7 +173,7 @@ describe('plan/update', function () { product: 'did:web:lite.web3.storage', }, proofs: [ - await Plan.update.delegate({ + await Plan.set.delegate({ issuer: agent, audience: bob, with: account, @@ -182,7 +182,7 @@ describe('plan/update', function () { ], }) const result = await access(await invocation.delegate(), { - capability: Plan.update, + capability: Plan.set, principal: Verifier, authority: service, validateAuthorization, @@ -191,13 +191,13 @@ describe('plan/update', function () { assert.fail(`error in self issue: ${result.error.message}`) } else { assert.deepEqual(result.ok.audience.did(), service.did()) - assert.equal(result.ok.capability.can, 'plan/update') + assert.equal(result.ok.capability.can, 'plan/set') assert.deepEqual(result.ok.capability.with, account) } }) - it('can invoke plan/update with the product that its delegation specifies', async function () { - const invocation = Plan.update.invoke({ + it('can invoke plan/set with the product that its delegation specifies', async function () { + const invocation = Plan.set.invoke({ issuer: bob, audience: service, with: account, @@ -205,7 +205,7 @@ describe('plan/update', function () { product: 'did:web:lite.web3.storage', }, proofs: [ - await Plan.update.delegate({ + await Plan.set.delegate({ issuer: agent, audience: bob, with: account, @@ -217,7 +217,7 @@ describe('plan/update', function () { ], }) const result = await access(await invocation.delegate(), { - capability: Plan.update, + capability: Plan.set, principal: Verifier, authority: service, validateAuthorization, @@ -226,13 +226,13 @@ describe('plan/update', function () { assert.fail(`error in self issue: ${result.error.message}`) } else { assert.deepEqual(result.ok.audience.did(), service.did()) - assert.equal(result.ok.capability.can, 'plan/update') + assert.equal(result.ok.capability.can, 'plan/set') assert.deepEqual(result.ok.capability.with, account) } }) - it('cannot invoke plan/update with a different product than its delegation specifies', async function () { - const invocation = Plan.update.invoke({ + it('cannot invoke plan/set with a different product than its delegation specifies', async function () { + const invocation = Plan.set.invoke({ issuer: bob, audience: service, with: account, @@ -240,7 +240,7 @@ describe('plan/update', function () { product: 'did:web:lite.web3.storage', }, proofs: [ - await Plan.update.delegate({ + await Plan.set.delegate({ issuer: agent, audience: bob, with: account, @@ -252,7 +252,7 @@ describe('plan/update', function () { ], }) const result = await access(await invocation.delegate(), { - capability: Plan.update, + capability: Plan.set, principal: Verifier, authority: service, validateAuthorization, diff --git a/packages/upload-api/src/plan/set.js b/packages/upload-api/src/plan/set.js new file mode 100644 index 000000000..dc7146cb3 --- /dev/null +++ b/packages/upload-api/src/plan/set.js @@ -0,0 +1,18 @@ +import * as API from '../types.js' +import * as Provider from '@ucanto/server' +import { Plan } from '@web3-storage/capabilities' + +/** + * @param {API.PlanServiceContext} context + */ +export const provide = (context) => + Provider.provide(Plan.set, (input) => set(input, context)) + +/** + * @param {API.Input} input + * @param {API.PlanServiceContext} context + * @returns {Promise>} + */ +const set = async ({ capability }, context) => { + return context.plansStorage.set(capability.with, capability.nb.product) +} diff --git a/packages/upload-api/src/types/plans.ts b/packages/upload-api/src/types/plans.ts index b5a29507a..a94a204ae 100644 --- a/packages/upload-api/src/types/plans.ts +++ b/packages/upload-api/src/types/plans.ts @@ -1,5 +1,5 @@ import * as Ucanto from '@ucanto/interface' -import { AccountDID, DID, PlanGetFailure, PlanGetSuccess } from '../types.js' +import { AccountDID, DID, PlanGetFailure, PlanGetSuccess, PlanSetFailure, PlanSetSuccess } from '../types.js' export type PlanID = DID @@ -17,12 +17,13 @@ export interface PlansStorage { ) => Promise> /** - * Set an account's plan + * Set an account's plan. Update our systems and any third party billing systems. * * @param account account DID + * @param plan the DID of the new plan */ set: ( account: AccountDID, plan: DID - ) => Promise> + ) => Promise> }