From 0916ba6bda8ad46ccc4f6bb0c6f4a48dd99db0c8 Mon Sep 17 00:00:00 2001 From: Hugo Dias Date: Tue, 30 Aug 2022 19:03:36 +0100 Subject: [PATCH] feat: fail validate for register email and add metrics --- packages/access-api/package.json | 3 +- packages/access-api/src/bindings.d.ts | 11 +++ packages/access-api/src/ucanto/service.js | 14 ++++ packages/access-api/test/helpers/setup.js | 71 +++++++++++++++++++ .../access-api/test/identity-validate.test.js | 21 ++++++ packages/access-api/wrangler.toml | 5 ++ packages/access/src/cli.js | 11 ++- 7 files changed, 133 insertions(+), 3 deletions(-) diff --git a/packages/access-api/package.json b/packages/access-api/package.json index 171db72a9..37517a45c 100644 --- a/packages/access-api/package.json +++ b/packages/access-api/package.json @@ -66,7 +66,8 @@ "DEBUG": "readonly", "ACCOUNTS": "writable", "VALIDATIONS": "writable", - "BUCKET": "writable" + "BUCKET": "writable", + "W3ACCESS_METRICS": "writable" } }, "eslintIgnore": [ diff --git a/packages/access-api/src/bindings.d.ts b/packages/access-api/src/bindings.d.ts index deb1d5b69..d9291475e 100644 --- a/packages/access-api/src/bindings.d.ts +++ b/packages/access-api/src/bindings.d.ts @@ -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 +} + declare global { const ACCOUNTS: KVNamespace const VALIDATIONS: KVNamespace + const W3ACCESS_METRICS: AnalyticsEngine } export interface RouteContext { diff --git a/packages/access-api/src/ucanto/service.js b/packages/access-api/src/ucanto/service.js index 9b443bb6b..3c8dd18c8 100644 --- a/packages/access-api/src/ucanto/service.js +++ b/packages/access-api/src/ucanto/service.js @@ -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 @@ -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, @@ -26,6 +34,7 @@ export function service(ctx) { caveats: { as: capability.with, }, + lifetimeInSeconds: 300, }) .delegate() @@ -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 }) => { diff --git a/packages/access-api/test/helpers/setup.js b/packages/access-api/test/helpers/setup.js index 409727f06..d64a5fbe6 100644 --- a/packages/access-api/test/helpers/setup.js +++ b/packages/access-api/test/helpers/setup.js @@ -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({ @@ -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({ @@ -34,6 +37,23 @@ export const mf = new Miniflare({ bindings, }) +export function createAnalyticsEngine() { + /** @type {Map} */ + 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) /** @@ -58,3 +78,54 @@ export function connection(id) { fetch: mf.dispatchFetch.bind(mf), }) } + +/** + * @param {import("@ucanto/interface").ConnectionView} 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} con + * @param {import("@ucanto/interface").SigningAuthority<237>} kp + * @param {import("@ucanto/interface").Proof<[UCAN.Capability, ...UCAN.Capability[]]>} 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) +} diff --git a/packages/access-api/test/identity-validate.test.js b/packages/access-api/test/identity-validate.test.js index 9ecb9c327..e5c88cae9 100644 --- a/packages/access-api/test/identity-validate.test.js +++ b/packages/access-api/test/identity-validate.test.js @@ -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' @@ -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, 'test-validate-fail@dag.house') + await register(con, kp, proof) + + try { + await validateEmail(con, kp, 'test-validate-fail@dag.house') + 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() diff --git a/packages/access-api/wrangler.toml b/packages/access-api/wrangler.toml index 5922d24de..55a98aee1 100644 --- a/packages/access-api/wrangler.toml +++ b/packages/access-api/wrangler.toml @@ -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" diff --git a/packages/access/src/cli.js b/packages/access/src/cli.js index 4586c5d86..665a9d14b 100755 --- a/packages/access/src/cli.js +++ b/packages/access/src/cli.js @@ -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( @@ -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) @@ -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: { @@ -104,6 +110,7 @@ prog }) await Access.register({ + audience: did, url, issuer, proof,