Skip to content

Commit

Permalink
feat: account recover with email (#149)
Browse files Browse the repository at this point in the history
Theres also a commit with changes to rename access to access-client

Co-authored-by: Alan Shaw <[email protected]>
  • Loading branch information
hugomrdias and Alan Shaw authored Nov 16, 2022
1 parent 37788b1 commit 91ad47d
Show file tree
Hide file tree
Showing 75 changed files with 878 additions and 601 deletions.
2 changes: 1 addition & 1 deletion .github/release-please-config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"separate-pull-requests": true,
"packages": {
"packages/access": {},
"packages/access-client": {},
"packages/access-api": {},
"packages/upload-client": {},
"packages/wallet": {}
Expand Down
2 changes: 1 addition & 1 deletion .github/release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"packages/wallet": "1.0.0",
"packages/access": "5.0.2",
"packages/access-client": "5.0.2",
"packages/access-api": "3.0.0",
"packages/upload-client": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Access SDK
name: Access Client
env:
CI: true
FORCE_COLOR: 1
Expand All @@ -7,12 +7,12 @@ on:
branches:
- main
paths:
- 'packages/access/**'
- 'packages/access-client/**'
- '.github/workflows/access.yml'
- 'pnpm-lock.yaml'
pull_request:
paths:
- 'packages/access/**'
- 'packages/access-client/**'
- '.github/workflows/access.yml'
- 'pnpm-lock.yaml'
jobs:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ on:
default: 'access-api'
options:
- access-api
- access
- upload-client
- access-client
- wallet
environment:
description: 'Environment to deploy'
Expand Down Expand Up @@ -52,9 +52,9 @@ jobs:
with:
environment: ${{ github.event.inputs.environment }}
secrets: inherit
deploy-access:
deploy-access-client:
runs-on: ubuntu-latest
if: github.event.inputs.package == 'access'
if: github.event.inputs.package == 'access-client'
steps:
- uses: actions/checkout@v3
- uses: pnpm/[email protected]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
release-type: node
publish-access:
needs: release
if: contains(fromJson(needs.release.outputs.paths_released), 'packages/access')
if: contains(fromJson(needs.release.outputs.paths_released), 'packages/access-client')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "ucan-protocol",
"name": "w3protocol",
"version": "0.0.0",
"private": true,
"workspaces": [
Expand Down
5 changes: 2 additions & 3 deletions packages/access-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"dev": "scripts/cli.js dev",
"build": "scripts/cli.js build",
"check": "tsc --build",
"test": "tsc --build && ava --timeout 10s"
"test": "pnpm build && tsc --build && ava --timeout 10s"
},
"author": "Hugo Dias <[email protected]> (hugodias.me)",
"license": "(Apache-2.0 OR MIT)",
Expand Down Expand Up @@ -56,6 +56,7 @@
"git-rev-sync": "^3.0.1",
"hd-scripts": "^3.0.2",
"miniflare": "^2.10.0",
"p-wait-for": "^5.0.0",
"process": "^0.11.10",
"readable-stream": "^4.1.0",
"sade": "^1.7.4",
Expand Down Expand Up @@ -92,8 +93,6 @@
],
"ava": {
"failFast": true,
"concurrency": 1,
"workerThreads": false,
"files": [
"test/**/*.test.js"
],
Expand Down
2 changes: 1 addition & 1 deletion packages/access-api/sql/migrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function migrate(db) {
} catch (error) {
const err = /** @type {Error} */ (error)
// eslint-disable-next-line no-console
console.log({
console.error('D1 Error', {
message: err.message,
// @ts-ignore
cause: err.cause?.message,
Expand Down
29 changes: 24 additions & 5 deletions packages/access-api/src/kvs/accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class Accounts {
tableName: 'accounts',
fields: '*',
where: {
conditions: 'did =?1',
conditions: 'did=?1',
params: [did],
},
})
Expand All @@ -68,10 +68,12 @@ export class Accounts {
}

/**
* @param {string} email
* Save account delegation per email
*
* @param {`mailto:${string}`} email
* @param {Ucanto.Delegation<Ucanto.Capabilities>} delegation
*/
async saveAccount(email, delegation) {
async saveDelegation(email, delegation) {
const accs = /** @type {string[] | undefined} */ (
await this.kv.get(email, {
type: 'json',
Expand All @@ -90,10 +92,27 @@ export class Accounts {
}

/**
* @param {string} email
* Check if we have delegations for an email
*
* @param {`mailto:${string}`} email
*/
async hasAccounts(email) {
async hasDelegations(email) {
const r = await this.kv.get(email)
return Boolean(r)
}

/**
* @param {`mailto:${string}`} email
*/
async getDelegations(email) {
const r = await this.kv.get(email, { type: 'json' })

if (!r) {
return
}

return /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/access/types').Any]>[]} */ (
r
)
}
}
5 changes: 3 additions & 2 deletions packages/access-api/src/kvs/validations.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ export class Validations {
}

/**
* @param {string} ucan
* @template {import('@ucanto/interface').Capabilities} [T=import('@ucanto/interface').Capabilities]
* @param {import('@web3-storage/access/src/types').EncodedDelegation<T>} ucan
*/
async put(ucan) {
const delegation =
/** @type {import('@ucanto/interface').Delegation<[import('@web3-storage/access/capabilities/types').VoucherClaim]>} */ (
/** @type {import('@ucanto/interface').Delegation<T>} */ (
await stringToDelegation(ucan)
)

Expand Down
50 changes: 49 additions & 1 deletion packages/access-api/src/routes/validate-email.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@ import {
* @param {import('../bindings.js').RouteContext} env
*/
export async function validateEmail(req, env) {
if (req.query && req.query.ucan && req.query.mode === 'recover') {
return recover(req, env)
}
if (req.query && req.query.ucan) {
try {
const delegation = await env.kvs.validations.put(req.query.ucan)
const delegation = await env.kvs.validations.put(
/** @type {import('@web3-storage/access/src/types.js').EncodedDelegation<[import('@web3-storage/access/src/types.js').VoucherClaim]>} */ (
req.query.ucan
)
)

return new HtmlResponse(
(
Expand Down Expand Up @@ -48,3 +55,44 @@ export async function validateEmail(req, env) {
<ValidateEmailError msg={'Missing delegation in the URL.'} />
)
}

/**
* @param {import('@web3-storage/worker-utils/router').ParsedRequest} req
* @param {import('../bindings.js').RouteContext} env
*/
async function recover(req, env) {
try {
const delegation = await env.kvs.validations.put(
/** @type {import('@web3-storage/access/src/types.js').EncodedDelegation<[import('@web3-storage/access/src/types.js').AccountRecover]>} */ (
req.query.ucan
)
)

return new HtmlResponse(
(
<ValidateEmail
delegation={delegation}
ucan={req.query.ucan}
qrcode={await QRCode.toString(req.query.ucan, {
type: 'svg',
errorCorrectionLevel: 'M',
margin: 10,
})}
/>
)
)
} catch (error) {
const err = /** @type {Error} */ (error)

if (err.message.includes('Invalid expiration')) {
return new HtmlResponse(
<ValidateEmailError msg={'Email confirmation expired.'} />
)
}

env.log.error(err)
return new HtmlResponse(
<ValidateEmailError msg={'Oops something went wrong.'} />
)
}
}
2 changes: 1 addition & 1 deletion packages/access-api/src/routes/validate-ws.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import pRetry from 'p-retry'
const run = async (
/** @type {import('../kvs/validations').Validations} */ kv,
/** @type {WebSocket} */ server,
/** @type {any} */ did
/** @type {string} */ did
) => {
const d = await kv.get(did)

Expand Down
65 changes: 57 additions & 8 deletions packages/access-api/src/service/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import * as Account from '@web3-storage/access/capabilities/account'
import { voucherClaimProvider } from './voucher-claim.js'
import { voucherRedeemProvider } from './voucher-redeem.js'
import * as DID from '@ipld/dag-ucan/did'
import { delegationToString } from '@web3-storage/access/encoding'
import {
delegationToString,
stringToDelegation,
} from '@web3-storage/access/encoding'
import { any } from '@web3-storage/access/capabilities/any'

/**
* @param {import('../bindings').RouteContext} ctx
Expand All @@ -18,13 +22,50 @@ export function service(ctx) {
},

account: {
info: Server.provide(Account.info, async ({ capability }) => {
info: Server.provide(Account.info, async ({ capability, invocation }) => {
const results = await ctx.kvs.accounts.get(capability.with)
if (!results) {
throw new Failure('Account not found...')
return new Failure('Account not found.')
}
return results
}),
recover: Server.provide(
Account.recover,
async ({ capability, invocation }) => {
if (capability.with !== ctx.signer.did()) {
return new Failure(
`Resource ${
capability.with
} does not service did ${ctx.signer.did()}`
)
}

const encoded = await ctx.kvs.accounts.getDelegations(
capability.nb.identity
)
if (!encoded) {
return new Failure(
`No delegations found for ${capability.nb.identity}`
)
}

const results = []
for (const e of encoded) {
const proof = await stringToDelegation(e)
const del = await any.delegate({
audience: invocation.issuer,
issuer: ctx.signer,
with: proof.capabilities[0].with,
expiration: Infinity,
proofs: [proof],
})

results.push(await delegationToString(del))
}

return results
}
),

'recover-validation': Server.provide(
Account.recoverValidation,
Expand All @@ -33,9 +74,9 @@ export function service(ctx) {
// if yes send email with account/login
// if not error "no accounts for email X"

const email = capability.nb.email
if (!(await ctx.kvs.accounts.hasAccounts(email))) {
throw new Failure(
const email = capability.nb.identity
if (!(await ctx.kvs.accounts.hasDelegations(email))) {
return new Failure(
`No accounts found for email: ${email.replace('mailto:', '')}.`
)
}
Expand All @@ -46,21 +87,29 @@ export function service(ctx) {
audience: DID.parse(capability.with),
with: ctx.signer.did(),
lifetimeInSeconds: 60 * 10,
nb: {
identity: email,
},
proofs: [
await Account.recover.delegate({
audience: ctx.signer,
issuer: ctx.signer,
lifetimeInSeconds: 60 * 1000,
expiration: Infinity,
with: ctx.signer.did(),
nb: {
identity: 'mailto:*',
},
}),
],
})
.delegate()

const encoded = await delegationToString(inv)
const url = `${ctx.url.protocol}//${ctx.url.host}/validate-email?ucan=${encoded}&mode=recover`

// For testing
if (ctx.config.ENV === 'test') {
return encoded
return url
}
}
),
Expand Down
4 changes: 1 addition & 3 deletions packages/access-api/src/service/voucher-claim.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ export function voucherClaimProvider(ctx) {
return encoded
}

const url = `${ctx.url.protocol}//${
ctx.url.host
}/validate-email?ucan=${encoded}&did=${invocation.issuer.did()}`
const url = `${ctx.url.protocol}//${ctx.url.host}/validate-email?ucan=${encoded}`

await ctx.email.sendValidation({
to: capability.nb.identity.replace('mailto:', ''),
Expand Down
Loading

0 comments on commit 91ad47d

Please sign in to comment.