Skip to content

Commit

Permalink
feat: fail validate for register email and add metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
hugomrdias committed Aug 30, 2022
1 parent 788d3d9 commit 0916ba6
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 3 deletions.
3 changes: 2 additions & 1 deletion packages/access-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
"DEBUG": "readonly",
"ACCOUNTS": "writable",
"VALIDATIONS": "writable",
"BUCKET": "writable"
"BUCKET": "writable",
"W3ACCESS_METRICS": "writable"
}
},
"eslintIgnore": [
Expand Down
11 changes: 11 additions & 0 deletions packages/access-api/src/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,20 @@ import type { config } from './config'

export {}

// CF Analytics Engine types not available yet
export interface AnalyticsEngine {
writeDataPoint: (event: AnalyticsEngineEvent) => void
}

export interface AnalyticsEngineEvent {
readonly doubles?: number[]
readonly blobs?: Array<ArrayBuffer | string | null>
}

declare global {
const ACCOUNTS: KVNamespace
const VALIDATIONS: KVNamespace
const W3ACCESS_METRICS: AnalyticsEngine
}

export interface RouteContext {
Expand Down
14 changes: 14 additions & 0 deletions packages/access-api/src/ucanto/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
identityRegister,
identityValidate,
} from '@web3-storage/access/capabilities'
import { HTTPError } from '@web3-storage/worker-utils/error'

/**
* @param {import('../bindings').RouteContext} ctx
Expand All @@ -18,6 +19,13 @@ export function service(ctx) {
validate: Server.provide(
identityValidate,
async ({ capability, context, invocation }) => {
const accounts = new Accounts()

const email = await accounts.get(capability.caveats.as)
if (email) {
throw new HTTPError('Email already registered.', { status: 400 })
}

const delegation = await identityRegister
.invoke({
audience: invocation.issuer,
Expand All @@ -26,6 +34,7 @@ export function service(ctx) {
caveats: {
as: capability.with,
},
lifetimeInSeconds: 300,
})
.delegate()

Expand Down Expand Up @@ -56,6 +65,11 @@ export function service(ctx) {
capability.with,
invocation.cid
)

W3ACCESS_METRICS.writeDataPoint({
blobs: [ctx.config.ENV, 'new_account'],
doubles: [1],
})
}
),
identify: Server.provide(identityIdentify, async ({ capability }) => {
Expand Down
71 changes: 71 additions & 0 deletions packages/access-api/test/helpers/setup.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import * as UCAN from '@ipld/dag-ucan'
import { SigningAuthority } from '@ucanto/authority'
import anyTest from 'ava'
import { Delegation } from '@ucanto/core'
import { connection as w3connection } from '@web3-storage/access/connection'
import { Miniflare } from 'miniflare'
import dotenv from 'dotenv'
import path from 'path'
import { fileURLToPath } from 'url'
import * as caps from '@web3-storage/access/capabilities'
const __dirname = path.dirname(fileURLToPath(import.meta.url))

dotenv.config({
Expand All @@ -25,6 +27,7 @@ export const bindings = {
POSTMARK_TOKEN: process.env.POSTMARK_TOKEN || '',
SENTRY_DSN: process.env.SENTRY_DSN || '',
LOGTAIL_TOKEN: process.env.LOGTAIL_TOKEN || '',
W3ACCESS_METRICS: createAnalyticsEngine(),
}

export const mf = new Miniflare({
Expand All @@ -34,6 +37,23 @@ export const mf = new Miniflare({
bindings,
})

export function createAnalyticsEngine() {
/** @type {Map<string,import('../../src/bindings').AnalyticsEngineEvent>} */
const store = new Map()

return {
writeDataPoint: (
/** @type {import('../../src/bindings').AnalyticsEngineEvent} */ event
) => {
store.set(
`${Date.now()}${(Math.random() + 1).toString(36).slice(7)}`,
event
)
},
_store: store,
}
}

export const serviceAuthority = SigningAuthority.parse(bindings.PRIVATE_KEY)

/**
Expand All @@ -58,3 +78,54 @@ export function connection(id) {
fetch: mf.dispatchFetch.bind(mf),
})
}

/**
* @param {import("@ucanto/interface").ConnectionView<import('@web3-storage/access/src/types').Service>} con
* @param {import("@ucanto/interface").SigningAuthority<237>} kp
* @param {string} email
*/
export async function validateEmail(con, kp, email) {
const validate = caps.identityValidate.invoke({
audience: serviceAuthority,
issuer: kp,
caveats: {
as: `mailto:${email}`,
},
with: kp.did(),
})

const out = await validate.execute(con)
if (out?.error) {
throw out
}
// @ts-ignore
const ucan = UCAN.parse(
// @ts-ignore
out.delegation.replace('http://localhost:8787/validate?ucan=', '')
)
const root = await UCAN.write(ucan)
const proof = Delegation.create({ root })

return proof
}

/**
* @param {import("@ucanto/interface").ConnectionView<import('@web3-storage/access/src/types').Service>} con
* @param {import("@ucanto/interface").SigningAuthority<237>} kp
* @param {import("@ucanto/interface").Proof<[UCAN.Capability<UCAN.Ability, `${string}:${string}`>, ...UCAN.Capability<UCAN.Ability, `${string}:${string}`>[]]>} proof
*/
export async function register(con, kp, proof) {
const register = caps.identityRegister.invoke({
audience: serviceAuthority,
issuer: kp,
// @ts-ignore
with: proof.capabilities[0].with,
caveats: {
// @ts-ignore
as: proof.capabilities[0].as,
},
proofs: [proof],
})

await register.execute(con)
}
21 changes: 21 additions & 0 deletions packages/access-api/test/identity-validate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
test,
send,
connection,
validateEmail,
register,
} from './helpers/setup.js'
import * as UCAN from '@ipld/dag-ucan'
import { SigningAuthority } from '@ucanto/authority'
Expand Down Expand Up @@ -86,6 +88,25 @@ test('should route correctly to identity/validate', async (t) => {
])
})

test('should fail to identity/validate with a know email', async (t) => {
const kp = await SigningAuthority.generate()
const con = connection(kp)

const proof = await validateEmail(con, kp, '[email protected]')
await register(con, kp, proof)

try {
await validateEmail(con, kp, '[email protected]')
t.fail('should not validate')
} catch (error) {
t.deepEqual(
// @ts-ignore
error.message,
'service handler {can: "identity/validate"} error: Email already registered.'
)
}
})

// test('should route correctly to identity/validate and fail with proof', async (t) => {
// const { mf } = t.context
// const kp = await ucans.EdKeypair.create()
Expand Down
5 changes: 5 additions & 0 deletions packages/access-api/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ id = "5697e95e1aaa436788e6d697fd3350be"
binding = "VALIDATIONS"
id = "ea17f472b37a43d29c1faf7af9512e03"

[[env.dev.unsafe.bindings]]
type = "analytics_engine"
dataset = "W3ACCESS_METRICS"
name = "W3ACCESS_METRICS"

# Staging
[env.staging]
name = "w3access-staging"
Expand Down
11 changes: 9 additions & 2 deletions packages/access/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import * as Access from './index.js'
import path from 'path'
import undici from 'undici'
import { Transform } from 'stream'
// @ts-ignore
import * as DID from '@ipld/dag-ucan/did'

const NAME = 'w3access'
const pkg = JSON.parse(
Expand All @@ -23,8 +25,11 @@ const config = new Conf({
})

const prog = sade(NAME)
const url = process.env.URL || 'https://access-api.web3.storage'
// const did = process.env.DID || 'did:key:z6MksafxoiEHyRF6RsorjrLrEyFQPFDdN6psxtAfEsRcvDqx'
const url = process.env.URL || 'https://w3access-dev.protocol-labs.workers.dev'
const did = DID.parse(
// @ts-ignore - https://github.com/ipld/js-dag-ucan/issues/49
process.env.DID || 'did:key:z6MksafxoiEHyRF6RsorjrLrEyFQPFDdN6psxtAfEsRcvDqx'
)

prog.version(pkg.version)

Expand Down Expand Up @@ -90,6 +95,7 @@ prog
const issuer = Keypair.parse(config.get('private-key'))
const url = new URL(opts.url)
await Access.validate({
audience: did,
url,
issuer,
caveats: {
Expand All @@ -104,6 +110,7 @@ prog
})

await Access.register({
audience: did,
url,
issuer,
proof,
Expand Down

0 comments on commit 0916ba6

Please sign in to comment.