Skip to content

Commit

Permalink
feat!: Remove 0.8 caps and add account delegation to the service (#123)
Browse files Browse the repository at this point in the history
closes #117
closes #121
  • Loading branch information
hugomrdias authored Oct 27, 2022
1 parent 09e1129 commit 878f8c9
Show file tree
Hide file tree
Showing 49 changed files with 1,270 additions and 831 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"prettier": "2.7.1",
"simple-git-hooks": "^2.8.1",
"typescript": "^4.8.4",
"wrangler": "^2.1.12"
"wrangler": "^2.1.13"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
Expand Down
28 changes: 20 additions & 8 deletions packages/access-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"scripts": {
"lint": "tsc --build && eslint '**/*.{js,ts}' && prettier --check '**/*.{js,ts,yml,json}' --ignore-path ../../.gitignore",
"deploy": "wrangler publish",
"dev": "miniflare --modules --watch --debug --wrangler-env dev --env ../../.env",
"dev": "scripts/cli.js dev",
"build": "scripts/cli.js build",
"check": "tsc --build",
"test": "tsc --build && ava --timeout 10s"
Expand All @@ -30,23 +30,29 @@
"multiformats": "^9.8.1",
"nanoid": "^4.0.0",
"p-retry": "^5.1.1",
"preact": "^10.11.2",
"preact-render-to-string": "^5.2.6",
"qrcode": "^1.5.1",
"toucan-js": "^2.7.0",
"workers-qb": "^0.1.2"
},
"devDependencies": {
"@cloudflare/workers-types": "^3.16.0",
"@sentry/cli": "^2.7.0",
"@cloudflare/workers-types": "^3.18.0",
"@databases/escape-identifier": "^1.0.3",
"@databases/sql": "^3.2.0",
"@sentry/cli": "^2.8.0",
"@sentry/webpack-plugin": "^1.19.1",
"@types/assert": "^1.5.6",
"@types/git-rev-sync": "^2.0.0",
"@types/node": "^18.11.0",
"@types/node": "^18.11.7",
"@types/qrcode": "^1.5.0",
"assert": "^2.0.0",
"ava": "^4.3.3",
"ava": "^5.0.1",
"better-sqlite3": "7.6.2",
"buffer": "^6.0.3",
"delay": "^5.0.0",
"dotenv": "^16.0.3",
"esbuild": "^0.15.10",
"esbuild": "^0.15.12",
"execa": "^6.1.0",
"git-rev-sync": "^3.0.1",
"hd-scripts": "^3.0.2",
Expand All @@ -55,7 +61,7 @@
"readable-stream": "^4.1.0",
"sade": "^1.7.4",
"typescript": "4.8.4",
"wrangler": "^2.1.12"
"wrangler": "^2.1.13"
},
"eslintConfig": {
"extends": [
Expand All @@ -74,6 +80,9 @@
"BUCKET": "writable",
"W3ACCESS_METRICS": "writable",
"WebSocketPair": "readonly"
},
"rules": {
"unicorn/prefer-number-properties": "off"
}
},
"eslintIgnore": [
Expand All @@ -84,6 +93,8 @@
],
"ava": {
"failFast": true,
"concurrency": 1,
"workerThreads": false,
"files": [
"test/**/*.test.js"
],
Expand All @@ -92,7 +103,8 @@
"--experimental-vm-modules"
],
"ignoredByWatcher": [
"./dist/*"
"./dist/*",
"./wrangler/**"
]
}
}
26 changes: 25 additions & 1 deletion packages/access-api/scripts/cli.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env -S node --experimental-vm-modules --no-warnings
/* eslint-disable no-console */
import path from 'path'
import dotenv from 'dotenv'
Expand All @@ -8,8 +8,11 @@ import { fileURLToPath } from 'url'
import { build } from 'esbuild'
import Sentry from '@sentry/cli'
import { createRequire } from 'module'
import { Miniflare } from 'miniflare'

// @ts-ignore
import git from 'git-rev-sync'
import { migrate } from '../sql/migrate.js'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const require = createRequire(__dirname)
Expand Down Expand Up @@ -65,6 +68,9 @@ prog
},
minify: opts.env !== 'dev',
sourcemap: true,
jsxImportSource: 'preact',
jsx: 'automatic',
loader: { '.js': 'jsx' },
})

// Sentry release and sourcemap upload
Expand Down Expand Up @@ -98,4 +104,22 @@ prog
}
})

prog
.command('dev')
.describe('Start dev server.')
.action(async () => {
const mf = new Miniflare({
packagePath: true,
wranglerConfigPath: true,
wranglerConfigEnv: 'dev',
sourceMap: true,
modules: true,
watch: true,
envPath: path.resolve(__dirname, '../../../.env'),
})

const binds = await mf.getBindings()
const db = /** @type {D1Database} */ (binds.__D1_BETA__)
await migrate(db)
})
prog.parse(process.argv)
37 changes: 37 additions & 0 deletions packages/access-api/sql/migrate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { escapeSQLiteIdentifier } from '@databases/escape-identifier'
import sql from '@databases/sql'
import path from 'path'
import { fileURLToPath } from 'url'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

/** @type {import('@databases/sql').FormatConfig} */
const sqliteFormat = {
escapeIdentifier: (str) => escapeSQLiteIdentifier(str),
formatValue: (value) => ({ placeholder: '?', value }),
}

const migrations = [sql.file(`${__dirname}/tables.sql`)]

/**
* Probably should batch queries and use https://www.atdatabases.org/docs/split-sql-query to split inlined queries
*
* @see https://docs.google.com/document/d/1QpUryGBWaGbAIjkw2URwpV6Btp5S-XQVkBJJs85dLRc/edit#
*
* @param {D1Database} db
*/
export async function migrate(db) {
try {
for (const m of migrations) {
await db.exec(m.format(sqliteFormat).text.replace(/\n/g, ''))
}
} catch (error) {
const err = /** @type {Error} */ (error)
// eslint-disable-next-line no-console
console.log({
message: err.message,
// @ts-ignore
cause: err.cause?.message,
})
}
}
2 changes: 2 additions & 0 deletions packages/access-api/sql/reset.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP TABLE
IF EXISTS accounts;
21 changes: 10 additions & 11 deletions packages/access-api/sql/tables.sql
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
DROP TABLE IF EXISTS accounts;

CREATE TABLE accounts (
did TEXT NOT NULL PRIMARY KEY
, product TEXT NOT NULL
, email TEXT NOT NULL
, agent TEXT NOT NULL
, inserted_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
, updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
, UNIQUE(did)
)
CREATE TABLE
IF NOT EXISTS accounts (
did TEXT NOT NULL PRIMARY KEY,
product TEXT NOT NULL,
email TEXT NOT NULL,
agent TEXT NOT NULL,
inserted_at TEXT NOT NULL DEFAULT (strftime ('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime ('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE (did)
)
8 changes: 4 additions & 4 deletions packages/access-api/src/bindings.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Logging } from '@web3-storage/worker-utils/logging'
import type { Handler as _Handler } from '@web3-storage/worker-utils/router'
import type { SigningPrincipal } from '@ucanto/interface'
import type { config } from './config'
import type { Signer } from '@ucanto/interface'
import { Email } from './utils/email.js'
import { Accounts } from './kvs/accounts.js'
import { Validations } from './kvs/validations.js'
import { D1QB } from 'workers-qb'
import { loadConfig } from './config.js'

export {}

Expand Down Expand Up @@ -38,8 +38,8 @@ export interface Env {

export interface RouteContext {
log: Logging
keypair: SigningPrincipal
config: typeof config
signer: Signer
config: ReturnType<typeof loadConfig>
url: URL
email: Email
kvs: {
Expand Down
2 changes: 0 additions & 2 deletions packages/access-api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { postRaw } from './routes/raw.js'
import { postRoot } from './routes/root.js'
import { validateEmail } from './routes/validate-email.js'
import { validateWS } from './routes/validate-ws.js'
import { validate } from './routes/validate.js'
import { version } from './routes/version.js'
import { getContext } from './utils/context.js'

Expand All @@ -15,7 +14,6 @@ const r = new Router({ onNotFound: notFound })

r.add('options', '*', preflight)
r.add('get', '/version', version)
r.add('get', '/validate', validate)
r.add('get', '/validate-email', validateEmail)
r.add('get', '/validate-ws', validateWS)
r.add('post', '/', postRoot)
Expand Down
92 changes: 69 additions & 23 deletions packages/access-api/src/kvs/accounts.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
// @ts-ignore
// eslint-disable-next-line no-unused-vars
import * as Ucanto from '@ucanto/interface'
import { delegationToString } from '@web3-storage/access/encoding'

/**
* @typedef {{account: string, proof: string}} AccountValue
* @typedef {import('@web3-storage/access/types').Account} Account
*/

/**
Expand All @@ -9,45 +15,85 @@ export class Accounts {
/**
*
* @param {KVNamespace} kv
* @param {import('workers-qb').D1QB} db
*/
constructor(kv) {
constructor(kv, db) {
this.kv = kv
this.db = db
}

/**
*
* @param {string} issuerDID
* @param {string} resourceDID
* @param {import('@ucanto/interface').Link} proof
* @param {import('@web3-storage/access/capabilities/types').VoucherRedeem} capability
* @param {Ucanto.Invocation<import('@web3-storage/access/capabilities/types').VoucherRedeem>} invocation
*/
async register(issuerDID, resourceDID, proof) {
const did = await this.get(issuerDID)
if (did) {
throw new Error(`did: ${issuerDID} already registered.`)
}
const account = `did:ipld:${proof}`
await this.kv.put(
issuerDID,
JSON.stringify({ account, proof: proof.toString() })
)
await this.kv.put(
resourceDID,
JSON.stringify({ account, proof: proof.toString() })
)
async create(capability, invocation) {
await this.db.insert({
tableName: 'accounts',
data: {
did: capability.nb.account,
product: capability.nb.product,
email: capability.nb.identity.replace('mailto:', ''),
agent: invocation.issuer.did(),
},
})
}

/**
* Get account by DID
*
* @param {string} did
*/
async get(did) {
const value = /** @type {AccountValue} */ (
await this.kv.get(did, {
const { results } = await this.db.fetchOne({
tableName: 'accounts',
fields: '*',
where: {
conditions: 'did =?1',
params: [did],
},
})

if (!results) {
return
}

return /** @type {Account} */ ({
did: results.did,
agent: results.agent,
email: results.email,
product: results.product,
updated_at: results.update_at,
inserted_at: results.inserted_at,
})
}

/**
* @param {string} email
* @param {Ucanto.Delegation<Ucanto.Capabilities>} delegation
*/
async saveAccount(email, delegation) {
const accs = /** @type {string[] | undefined} */ (
await this.kv.get(email, {
type: 'json',
})
)

if (value) {
return value
if (accs) {
accs.push(await delegationToString(delegation))
await this.kv.put(email, JSON.stringify(accs))
} else {
await this.kv.put(
email,
JSON.stringify([await delegationToString(delegation)])
)
}
}

/**
* @param {string} email
*/
async hasAccounts(email) {
const r = await this.kv.get(email)
return Boolean(r)
}
}
Loading

0 comments on commit 878f8c9

Please sign in to comment.