Skip to content

Commit

Permalink
feat: ucanto invocation router with bare minimal store add invocation
Browse files Browse the repository at this point in the history
  • Loading branch information
vasco-santos committed Nov 11, 2022
1 parent 54f4888 commit e9e5124
Show file tree
Hide file tree
Showing 18 changed files with 13,161 additions and 9,271 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ jobs:
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm test
env:
AWS_REGION: 'us-east-1'
AWS_ACCESS_KEY_ID: 'NOSUCH'
AWS_SECRET_ACCESS_KEY: 'NOSUCH'
10 changes: 10 additions & 0 deletions api/authority.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as ed25519 from '@ucanto/principal/ed25519'

export default async function getServiceDid() {
// This is a Fixture for now, let's see how config is in current w3up project with secrets + env vars

/** did:key:z6MkrZ1r5XBFZjBU34qyD8fueMbMRkKw17BZaq2ivKFjnz2z */
return ed25519.parse(
'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8='
)
}
89 changes: 89 additions & 0 deletions api/databases/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
DynamoDBClient,
GetItemCommand,
PutItemCommand,
} from '@aws-sdk/client-dynamodb'
import { marshall } from '@aws-sdk/util-dynamodb'

/**
* @typedef {object} StoreItem
* @property {string} uploaderDID
* @property {string} payloadCID
* @property {string} applicationDID
* @property {string} origin
* @property {number} size
* @property {string} proof
* @property {string} uploadedAt
*/

export class StoreDatabase {
/**
* @param {string} region
* @param {string} tableName
* @param {object} [options]
* @param {string} [options.endpoint]
*/
constructor (region, tableName, options = {}) {
this.dynamoDb = new DynamoDBClient({
region,
endpoint: options.endpoint
})
this.tableName = tableName
}

/**
* Check if the given link CID is bound to the uploader account
*
* @param {string} uploaderDID
* @param {string} payloadCID
*/
async exists (uploaderDID, payloadCID) {
const params = {
TableName: this.tableName,
Key: marshall({
uploaderDID: uploaderDID.toString(),
payloadCID: payloadCID.toString(),
}),
AttributesToGet: ['uploaderDID'],
}

try {
const response = await this.dynamoDb.send(new GetItemCommand(params))
return response?.Item !== undefined
} catch {
return false
}
}

/**
* Bind a link CID to an account
*
* @param {object} data
* @param {string} data.accountDID
* @param {object} data.link
* @param {object} data.proof
* @param {string} data.origin
* @param {number} data.size
* @returns {Promise<StoreItem>}
*/
async insert({ accountDID, link, proof, origin, size = 0 }) {
const item = {
uploaderDID: accountDID?.toString(),
payloadCID: link?.toString(),
applicationDID: '',
origin: origin?.toString() || '',
size,
proof: proof?.toString(),
uploadedAt: new Date().toISOString(),
}

const params = {
TableName: this.tableName,
Item: marshall(item),
}

await this.dynamoDb.send(new PutItemCommand(params))

return item
}
}
87 changes: 87 additions & 0 deletions api/functions/ucan-invocation-router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as Server from '@ucanto/server'
import * as CAR from '@ucanto/transport/car'
import * as CBOR from '@ucanto/transport/cbor'

import getServiceDid from '../authority.js'
import { StoreDatabase } from '../databases/store.js'
import { createServiceRouter } from '../service/index.js'

/**
* AWS API Gateway handler for POST / with ucan invocation router.
*
* We provide responses in Payload format v2.0
* see: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format
*
* @param {import('aws-lambda').APIGatewayProxyEventV2} request
*/
async function ucanInvocationRouter (request) {
const {
STORE_TABLE_NAME: storeTableName = '',
AWS_SECRET_ACCESS_KEY: secretAccessKey = '',
AWS_ACCESS_KEY_ID: accessKeyId = '',
AWS_REGION: region = 'us-west-2',
AWS_SESSION_TOKEN: sessionToken = '',
CAR_BUCKET_NAME: bucket = '',
// set for testing
DYNAMO_DB_ENDPOINT: dbEndpoint
} = process.env

if (request.body === undefined) {
return {
statusCode: 400,
}
}

const server = await createUcantoServer({
storeDatabase: new StoreDatabase(region, storeTableName, {
endpoint: dbEndpoint
}),
signingOptions: {
region,
secretAccessKey,
accessKeyId,
bucket,
sessionToken,
}
})
const response = await server.request({
// @ts-ignore - type is Record<string, string|string[]|undefined>
headers: request.headers,
body: Buffer.from(request.body, 'base64'),
})

return toLambdaSuccessResponse(response)
}

export const handler = ucanInvocationRouter

/**
* @param {import('../service/types').UcantoServerContext} context
*/
export async function createUcantoServer (context) {
const id = await getServiceDid()
const server = Server.create({
id,
encoder: CBOR,
decoder: CAR,
service: createServiceRouter(context),
catch: (/** @type {string | Error} */ err) => {
// TODO: We need sentry to log stuff
console.log('err', err)
},
})

return server
}

/**
* @param {Server.HTTPResponse<never>} response
*/
function toLambdaSuccessResponse (response) {
return {
statusCode: 200,
headers: response.headers,
body: Buffer.from(response.body).toString('base64'),
isBase64Encoded: true,
}
}
2 changes: 1 addition & 1 deletion services/package.json → api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"test": "ava --verbose --timeout=60s"
"test": "ava --verbose --timeout=60s **/*.test.js"
},
"dependencies": {
"aws-sdk": "^2.1250.0"
Expand Down
12 changes: 12 additions & 0 deletions api/service/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createStoreService } from './store.js'

/**
* @param {import('./types').UcantoServerContext} context
* @returns {Record<string, any>}
*/
export function createServiceRouter (context) {
return {
store: createStoreService(context),
// TODO: upload
}
}
52 changes: 52 additions & 0 deletions api/service/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as Server from '@ucanto/server'
import * as Store from '@web3-storage/access/capabilities/store'

/**
* @param {import('./types').StoreServiceContext} context
*/
export function createStoreService (context) {
return {
add: Server.provide(
Store.add,
async ({ capability, invocation }) => {
const { link, origin, size } = capability.nb
const proof = invocation.cid

if (!link) {
return new Server.MalformedCapability(
invocation.capabilities[0],
new Server.Failure('Provided capability has no link')
)
}

// Only use capability account for now to check if account is registered.
// This must change to access account/info!!
// We need to use https://github.com/web3-storage/w3protocol/blob/9d4b5bec1f0e870233b071ecb1c7a1e09189624b/packages/access/src/agent.js#L270
const account = capability.with

// @ts-ignore link type
const carExists = await context.storeDatabase.exists(account, link)

if (!carExists) {
await context.storeDatabase.insert({
accountDID: account,
link,
proof,
// @ts-ignore
origin,
// @ts-ignore
size
})
}

// TODO: see if CAR exists in upload too and return done
return {
status: 'done',
with: account,
link
}
// TODO: upload return with url
}
)
}
}
19 changes: 19 additions & 0 deletions api/service/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { StoreDatabase } from '../databases/store'

export interface UcantoServerContext {
storeDatabase: StoreDatabase,
signingOptions: SigningOptions
}

export interface StoreServiceContext {
storeDatabase: StoreDatabase,
signingOptions: SigningOptions
}

export interface SigningOptions {
region: string,
secretAccessKey: string,
accessKeyId: string,
bucket: string,
sessionToken: string,
}
6 changes: 6 additions & 0 deletions api/test/fixtures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as ed25519 from '@ucanto/principal/ed25519'

/** did:key:z6Mkqa4oY9Z5Pf5tUcjLHLUsDjKwMC95HGXdE1j22jkbhz6r */
export const alice = ed25519.parse(
'MgCZT5vOnYZoVAeyjnzuJIVY9J4LNtJ+f8Js0cTPuKUpFne0BVEDJjEu6quFIU8yp91/TY/+MYK8GvlKoTDnqOCovCVM='
)
42 changes: 42 additions & 0 deletions api/test/functions/ucan-invocation-router.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import test from 'ava'
import { GenericContainer as Container } from 'testcontainers'

import { parse } from '@ipld/dag-ucan/did'
import { CAR } from '@ucanto/transport'

import getServiceDid from '../../authority.js'
import { handler } from '../../functions/ucan-invocation-router.js'

import { alice } from '../fixtures.js'

test.before(async t => {
await new Container('amazon/dynamodb-local:latest')
.withExposedPorts(8000)
.start()
})

// TODO: Need to set ENV for dbEndpoint...
test.skip('ucan-invocation-router', async (t) => {
const serviceDid = await getServiceDid()

const account = alice.did()
const bytes = new Uint8Array([11, 22, 34, 44, 55])
const link = await CAR.codec.link(bytes)

const request = await CAR.encode([
{
issuer: alice,
audience: parse(serviceDid.did()),
capabilities: [{
can: 'store/add',
with: account,
nb: { link },
}],
proofs: [],
}
])

// @ts-ignore convert to AWS type?
const storeAddResponse = await handler(request)
t.is(storeAddResponse.statusCode, 200)
})
Loading

0 comments on commit e9e5124

Please sign in to comment.