Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: store ucan blocks per invocation #92

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions upload-api/functions/ucan-invocation-router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
}),
Expand All @@ -74,17 +75,14 @@ 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<string, string|string[]|undefined>
headers: request.headers,
body: Buffer.from(request.body, 'base64'),
})

// persist successful invocation handled
await persistUcanInvocation(request, ucanStoreBucket)

return toLambdaSuccessResponse(response)
}

Expand Down
12 changes: 7 additions & 5 deletions upload-api/service/index.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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) {
Expand All @@ -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)
Expand Down
84 changes: 84 additions & 0 deletions upload-api/service/server.js
Original file line number Diff line number Diff line change
@@ -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<string, any>} Service
* @param {API.Server<Service> & import('./types').UcantoServerContext} options
* @returns {API.ServerView<Service>}
*/
export const create = options => new Server(options)

/**
* @template {Record<string, any>} Service
* @implements {API.ServerView<Service>}
*/
class Server {
/**
* @param {API.Server<Service> & 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<API.ServiceInvocation<C, Service>>} I
* @param {API.HTTPRequest<I>} request
* @returns {API.Await<API.HTTPResponse<API.InferServiceInvocations<I, Service>>>}
*/
request(request) {
return handle(/** @type {API.ServerView<Service>} */ (this), request, this.ucanBucket)
}
}

/**
* @template {Record<string, any>} T
* @template {API.Capability} C
* @template {API.Tuple<API.ServiceInvocation<C, T>>} I
* @param {API.ServerView<T>} server
* @param {API.HTTPRequest<I>} request
* @param {import('./types').UcanBucket} ucanBucket
* @returns {Promise<API.HTTPResponse<API.InferServiceInvocations<I, T>>>}
*/
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
}
6 changes: 5 additions & 1 deletion upload-api/service/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>
Expand Down
21 changes: 12 additions & 9 deletions upload-api/test/helpers/ucanto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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)
})
}

Expand Down