From df7d1730f8236646494c2960a0ae3858dd633c2a Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 13 Dec 2022 10:42:55 +0100 Subject: [PATCH] feat: store ucan blocks per invocation --- .../functions/ucan-invocation-router.js | 10 +-- upload-api/service/index.js | 12 +-- upload-api/service/server.js | 84 +++++++++++++++++++ upload-api/service/types.ts | 6 +- upload-api/test/helpers/ucanto.js | 21 +++-- 5 files changed, 112 insertions(+), 21 deletions(-) create mode 100644 upload-api/service/server.js diff --git a/upload-api/functions/ucan-invocation-router.js b/upload-api/functions/ucan-invocation-router.js index 17fe8050..c5a18360 100644 --- a/upload-api/functions/ucan-invocation-router.js +++ b/upload-api/functions/ucan-invocation-router.js @@ -2,7 +2,6 @@ import { DID } from '@ucanto/core' import * as Sentry from '@sentry/serverless' import { createAccessClient } from '../access.js' -import { persistUcanInvocation } from '../ucan-invocation.js' import { createCarStore } from '../buckets/car-store.js' import { createDudewhereStore } from '../buckets/dudewhere-store.js' import { createUcanStore } from '../buckets/ucan-store.js' @@ -53,9 +52,11 @@ async function ucanInvocationRouter (request) { } const serviceSigner = getServiceSigner() - const ucanStoreBucket = createUcanStore(AWS_REGION, ucanBucketName) + const ucanBucket = createUcanStore(AWS_REGION, ucanBucketName) const server = await createUcantoServer(serviceSigner, { + ucanBucket + }, { storeTable: createStoreTable(AWS_REGION, storeTableName, { endpoint: dbEndpoint }), @@ -74,7 +75,7 @@ async function ucanInvocationRouter (request) { uploadTable: createUploadTable(AWS_REGION, uploadTableName, { endpoint: dbEndpoint }), - access: createAccessClient(serviceSigner, DID.parse(accessServiceDID), new URL(accessServiceURL)) + access: createAccessClient(serviceSigner, DID.parse(accessServiceDID), new URL(accessServiceURL)), }) const response = await server.request({ // @ts-expect-error - type is Record @@ -82,9 +83,6 @@ async function ucanInvocationRouter (request) { body: Buffer.from(request.body, 'base64'), }) - // persist successful invocation handled - await persistUcanInvocation(request, ucanStoreBucket) - return toLambdaSuccessResponse(response) } diff --git a/upload-api/service/index.js b/upload-api/service/index.js index 63743138..494454da 100644 --- a/upload-api/service/index.js +++ b/upload-api/service/index.js @@ -1,4 +1,4 @@ -import * as Server from '@ucanto/server' +import * as Server from './server.js' import * as CAR from '@ucanto/transport/car' import * as CBOR from '@ucanto/transport/cbor' import * as Sentry from '@sentry/serverless' @@ -12,7 +12,7 @@ Sentry.AWSLambda.init({ }) /** - * @param {import('./types').UcantoServerContext} context + * @param {import('./types').UcantoServiceContext} context * @returns {import('./types').Service} */ export function createServiceRouter (context) { @@ -24,14 +24,16 @@ export function createServiceRouter (context) { /** * @param {import('@ucanto/interface').Signer} serviceSigner - * @param {import('../service/types').UcantoServerContext} context + * @param {import('../service/types').UcantoServerContext} serverContext + * @param {import('../service/types').UcantoServiceContext} serviceContext */ - export async function createUcantoServer (serviceSigner, context) { + export async function createUcantoServer (serviceSigner, serverContext, serviceContext) { const server = Server.create({ id: serviceSigner, encoder: CBOR, decoder: CAR, - service: createServiceRouter(context), + service: createServiceRouter(serviceContext), + ...serverContext, catch: (/** @type {string | Error} */ err) => { console.warn(err) Sentry.AWSLambda.captureException(err) diff --git a/upload-api/service/server.js b/upload-api/service/server.js new file mode 100644 index 00000000..15d10333 --- /dev/null +++ b/upload-api/service/server.js @@ -0,0 +1,84 @@ +/** + * This is an implementation of ucanto server based on @ucanto/server. + * After iterating here, we should move into + */ + +// eslint-disable-next-line no-unused-vars +import * as API from '@ucanto/interface' +import { Verifier } from '@ucanto/principal' +import { execute } from '@ucanto/server' + +import { persistUcanInvocation } from '../ucan-invocation.js' + +/** + * Creates a connection to a service. + * + * @template {Record} Service + * @param {API.Server & import('./types').UcantoServerContext} options + * @returns {API.ServerView} + */ +export const create = options => new Server(options) + +/** + * @template {Record} Service + * @implements {API.ServerView} + */ +class Server { + /** + * @param {API.Server & import('./types').UcantoServerContext} options + */ + constructor({ + id, + service, + encoder, + decoder, + principal = Verifier, + ucanBucket, + canIssue = (capability, issuer) => + capability.with === issuer || issuer === id.did(), + ...rest + }) { + const { catch: fail, ...context } = rest + this.context = { id, principal, canIssue, ...context } + this.service = service + this.encoder = encoder + this.decoder = decoder + this.ucanBucket = ucanBucket + this.catch = fail || (() => {}) + } + + get id() { + return this.context.id + } + + /** + * @template {API.Capability} C + * @template {API.Tuple>} I + * @param {API.HTTPRequest} request + * @returns {API.Await>>} + */ + request(request) { + return handle(/** @type {API.ServerView} */ (this), request, this.ucanBucket) + } +} + +/** + * @template {Record} T + * @template {API.Capability} C + * @template {API.Tuple>} I + * @param {API.ServerView} server + * @param {API.HTTPRequest} request + * @param {import('./types').UcanBucket} ucanBucket + * @returns {Promise>>} + */ +export const handle = async (server, request, ucanBucket) => { + const invocations = await server.decoder.decode(request) + const result = await execute(invocations, server) + const response = server.encoder.encode(result) + + // persist successful invocation handled + // @ts-expect-error AWS request types are different + await persistUcanInvocation(request, ucanBucket) + + return response +} diff --git a/upload-api/service/types.ts b/upload-api/service/types.ts index ebd5f10b..1c9dcd91 100644 --- a/upload-api/service/types.ts +++ b/upload-api/service/types.ts @@ -43,7 +43,11 @@ export interface UploadServiceContext { dudewhereBucket: DudewhereBucket } -export interface UcantoServerContext extends StoreServiceContext, UploadServiceContext {} +export interface UcantoServiceContext extends StoreServiceContext, UploadServiceContext {} + +export interface UcantoServerContext { + ucanBucket: UcanBucket, +} export interface CarStoreBucket { has: (link: AnyLink) => Promise diff --git a/upload-api/test/helpers/ucanto.js b/upload-api/test/helpers/ucanto.js index 411c3d15..4017e5fe 100644 --- a/upload-api/test/helpers/ucanto.js +++ b/upload-api/test/helpers/ucanto.js @@ -4,6 +4,7 @@ import * as Signer from '@ucanto/principal/ed25519' import { createUcantoServer } from '../../service/index.js' import { createCarStore } from '../../buckets/car-store.js' +import { createUcanStore } from '../../buckets/ucan-store.js' import { createDudewhereStore } from '../../buckets/dudewhere-store.js' import { createStoreTable } from '../../tables/store.js' import { createUploadTable } from '../../tables/upload.js' @@ -24,15 +25,17 @@ import { createAccessClient } from '../../access.js' export function createTestingUcantoServer(service, ctx) { const region = ctx.region || 'us-west-2' return createUcantoServer(service, { - storeTable: createStoreTable(region, ctx.tableName, { - endpoint: ctx.dbEndpoint - }), - uploadTable: createUploadTable(region, ctx.tableName, { - endpoint: ctx.dbEndpoint - }), - carStoreBucket: createCarStore(region, ctx.bucketName, { ...ctx.s3ClientOpts }), - dudewhereBucket: createDudewhereStore(region, ctx.bucketName, { ...ctx.s3ClientOpts }), - access: createAccessClient(service, ctx.access.servicePrincipal, ctx.access.serviceURL) + ucanBucket: createUcanStore(region, ctx.bucketName, { ...ctx.s3ClientOpts }) + }, { + storeTable: createStoreTable(region, ctx.tableName, { + endpoint: ctx.dbEndpoint + }), + uploadTable: createUploadTable(region, ctx.tableName, { + endpoint: ctx.dbEndpoint + }), + carStoreBucket: createCarStore(region, ctx.bucketName, { ...ctx.s3ClientOpts }), + dudewhereBucket: createDudewhereStore(region, ctx.bucketName, { ...ctx.s3ClientOpts }), + access: createAccessClient(service, ctx.access.servicePrincipal, ctx.access.serviceURL) }) }