-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ucanto invocation router with bare minimal store add invocation
- Loading branch information
1 parent
54f4888
commit e9e5124
Showing
18 changed files
with
13,161 additions
and
9,271 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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=' | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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=' | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) |
Oops, something went wrong.