Skip to content

Commit

Permalink
feat!: provision using a proof (#123)
Browse files Browse the repository at this point in the history
Adds a couple of more commands

`w3 coupon create did:...` - That can be used to create delegation and
pack it as a redeemable coupon.
`w3 space provision --coupon https://gozala.io/coupon` - That can be
used to provision space with pre-arranged coupon
`w3 plan get` - Prints current plan info

All the above are in support of the workshop and specifically so we
could create short lived coupon for workshop participants that would
allow them to provision spaces without signing up as customer.
  • Loading branch information
Gozala authored Nov 16, 2023
1 parent c40e5df commit d61bdf3
Show file tree
Hide file tree
Showing 10 changed files with 514 additions and 114 deletions.
29 changes: 27 additions & 2 deletions bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getPkg } from './lib.js'
import {
Account,
Space,
Coupon,
accessClaim,
addSpace,
listSpaces,
Expand All @@ -21,7 +22,8 @@ import {
remove,
list,
whoami,
usageReport
usageReport,
getPlan,
} from './index.js'
import {
storeAdd,
Expand All @@ -30,7 +32,7 @@ import {
uploadAdd,
uploadList,
uploadRemove,
filecoinInfo
filecoinInfo,
} from './can.js'

const pkg = getPkg()
Expand All @@ -52,6 +54,12 @@ cli
)
.action(Account.login)

cli
.command('plan get [email]')
.example('plan get user@example.com')
.describe('Displays plan given account is on')
.action(getPlan)

cli
.command('account ls')
.alias('account list')
Expand Down Expand Up @@ -122,6 +130,8 @@ cli
.command('space provision [name]')
.describe('Associating space with a billing account')
.option('-c, --customer', 'The email address of the billing account')
.option('--coupon', 'Coupon URL to provision space with')
.option('-p, -password', 'Coupon password')
.option(
'-p, --provider',
'The storage provider to associate with this space.'
Expand Down Expand Up @@ -152,6 +162,21 @@ cli
.describe('Set the current space in use by the agent')
.action(useSpace)

cli
.command('coupon create <did>')
.option('--password', 'Password for created coupon.')
.option('-c, --can', 'One or more abilities to delegate.')
.option(
'-e, --expiration',
'Unix timestamp when the delegation is no longer valid. Zero indicates no expiration.',
0
)
.option(
'-o, --output',
'Path of file to write the exported delegation data to.'
)
.action(Coupon.issue)

cli
.command('delegation create <audience-did>')
.describe(
Expand Down
55 changes: 55 additions & 0 deletions coupon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import fs from 'node:fs/promises'
import * as DID from '@ipld/dag-ucan/did'
import * as Account from './account.js'
import * as Space from './space.js'
import { getClient } from './lib.js'
import * as ucanto from '@ucanto/core'

export { Account, Space }

/**
* @typedef {object} CouponIssueOptions
* @property {string} customer
* @property {string[]|string} [can]
* @property {string} [password]
* @property {number} [expiration]
* @property {string} [output]
*
* @param {string} customer
* @param {CouponIssueOptions} options
*/
export const issue = async (
customer,
{ can = 'provider/add', expiration, password, output }
) => {
const client = await getClient()

const audience = DID.parse(customer)
const abilities = can ? [can].flat() : []
if (!abilities.length) {
console.error('Error: missing capabilities for delegation')
process.exit(1)
}

const capabilities = /** @type {ucanto.API.Capabilities} */ (
abilities.map((can) => ({ can, with: audience.did() }))
)

const coupon = await client.coupon.issue({
capabilities,
expiration: expiration === 0 ? Infinity : expiration,
password,
})

const { ok: bytes, error } = await coupon.archive()
if (!bytes) {
console.error(error)
return process.exit(1)
}

if (output) {
await fs.writeFile(output, bytes)
} else {
process.stdout.write(bytes)
}
}
58 changes: 49 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from './lib.js'
import * as ucanto from '@ucanto/core'
import chalk from 'chalk'

export * as Coupon from './coupon.js'
export { Account, Space }

/**
Expand All @@ -31,6 +31,31 @@ export async function accessClaim() {
await client.capability.access.claim()
}

/**
* @param {string} email
*/
export const getPlan = async (email = '') => {
const client = await getClient()
const account =
email === ''
? await Space.selectAccount(client)
: await Space.useAccount(client, { email })

if (account) {
const { ok: plan, error } = await account.plan.get()
if (plan) {
console.log(`⁂ ${plan.product}`)
} else if (error?.name === 'PlanNotFound') {
console.log('⁂ no plan has been selected yet')
} else {
console.error(`Failed to get plan - ${error.message}`)
process.exit(1)
}
} else {
process.exit(1)
}
}

/**
* @param {`${string}@${string}`} email
* @param {object} [opts]
Expand Down Expand Up @@ -95,8 +120,8 @@ export async function upload(firstPath, opts) {
const uploadFn = opts?.car
? client.uploadCAR.bind(client, files[0])
: files.length === 1 && opts?.['no-wrap']
? client.uploadFile.bind(client, files[0])
: client.uploadDirectory.bind(client, files)
? client.uploadFile.bind(client, files[0])
: client.uploadDirectory.bind(client, files)

const root = await uploadFn({
onShardStored: ({ cid, size, piece }) => {
Expand Down Expand Up @@ -492,18 +517,31 @@ export async function usageReport(opts) {
const period = {
// we may not have done a snapshot for this month _yet_, so get report from last month -> now
from: startOfLastMonth(now),
to: now
to: now,
}

let total = 0
for await (const { account, provider, space, size } of getSpaceUsageReports(client, period)) {
for await (const { account, provider, space, size } of getSpaceUsageReports(
client,
period
)) {
if (opts?.json) {
console.log(dagJSON.stringify({ account, provider, space, size, reportedAt: now.toISOString() }))
console.log(
dagJSON.stringify({
account,
provider,
space,
size,
reportedAt: now.toISOString(),
})
)
} else {
console.log(` Account: ${account}`)
console.log(`Provider: ${provider}`)
console.log(` Space: ${space}`)
console.log(` Size: ${opts?.human ? filesize(size.final) : size.final}\n`)
console.log(
` Size: ${opts?.human ? filesize(size.final) : size.final}\n`
)
}
total += size.final
}
Expand All @@ -516,9 +554,11 @@ export async function usageReport(opts) {
* @param {import('@web3-storage/w3up-client').Client} client
* @param {{ from: Date, to: Date }} period
*/
async function * getSpaceUsageReports (client, period) {
async function* getSpaceUsageReports(client, period) {
for (const account of Object.values(client.accounts())) {
const subscriptions = await client.capability.subscription.list(account.did())
const subscriptions = await client.capability.subscription.list(
account.did()
)
for (const { consumers } of subscriptions.results) {
for (const space of consumers) {
const result = await client.capability.usage.report(space, period)
Expand Down
Loading

0 comments on commit d61bdf3

Please sign in to comment.