From 5d8fd55ce05aff315aae9c3cd2ae27ad9494dbdf Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 17 May 2023 14:42:00 +0200 Subject: [PATCH] feat: add ucanto aggregation api service --- package.json | 3 +- packages/core/package.json | 20 +++++ packages/core/service.ts | 30 +++++++ packages/core/tsconfig.json | 6 ++ packages/functions/package.json | 3 + .../src/api/ucan-invocation-router.ts | 47 +++++++++++ packages/functions/sst-env.d.ts | 1 + pnpm-lock.yaml | 81 +++++++++++++++++++ sst.config.ts | 2 + stacks/ApiStack.ts | 16 +++- stacks/DataStack.ts | 17 ++++ 11 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 packages/core/package.json create mode 100644 packages/core/service.ts create mode 100644 packages/core/tsconfig.json create mode 100644 packages/functions/src/api/ucan-invocation-router.ts create mode 100644 packages/functions/sst-env.d.ts create mode 100644 stacks/DataStack.ts diff --git a/package.json b/package.json index 7778003..b6e663e 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "deploy": "sst deploy", "remove": "sst remove", "console": "sst console", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "clean": "rm -rf node_modules pnpm-lock.yml packages/*/{pnpm-lock.yml,.next,out,coverage,.nyc_output,worker,dist,node_modules}" }, "devDependencies": { "@sentry/serverless": "^7.52.1", diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..fad64e3 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,20 @@ +{ + "name": "@spade-proxy/core", + "version": "0.0.0", + "type": "module", + "scripts": { + "test": "sst bind vitest", + "typecheck": "tsc -noEmit" + }, + "dependencies": { + "@ipld/dag-ucan": "3.3.2", + "@ucanto/interface": "7.1.0", + "@ucanto/principal": "^7.0.0" + }, + "devDependencies": { + "@types/node": "^18.16.3", + "vitest": "^0.31.0", + "sst": "^2.8.3", + "typescript": "^5.0.4" + } +} \ No newline at end of file diff --git a/packages/core/service.ts b/packages/core/service.ts new file mode 100644 index 0000000..02a135f --- /dev/null +++ b/packages/core/service.ts @@ -0,0 +1,30 @@ +import * as ed25519 from '@ucanto/principal/ed25519' +import * as DID from '@ipld/dag-ucan/did' +import { Signer } from '@ucanto/interface' + +export const createUcantoServer = (servicePrincipal: Signer, context: UcantoServerCtx) => { + +} + +/** + * Given a config, return a ucanto Signer object representing the service + */ + export function getServiceSigner(config: ServiceSignerCtx) { + const signer = ed25519.parse(config.PRIVATE_KEY) + if (config.SPADE_PROXY_DID) { + const did = DID.parse(config.SPADE_PROXY_DID).did() + return signer.withDID(did) + } + return signer +} + +export interface UcantoServerCtx { + +} + +export type ServiceSignerCtx = { + // multiformats private key of primary signing key + PRIVATE_KEY: string + // public DID for the upload service (did:key:... derived from PRIVATE_KEY if not set) + SPADE_PROXY_DID: string +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..3a1245d --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@tsconfig/node16/tsconfig.json", + "compilerOptions": { + "module": "esnext" + } +} diff --git a/packages/functions/package.json b/packages/functions/package.json index ff0f54e..ddf26f5 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -6,6 +6,9 @@ "test": "sst bind vitest", "typecheck": "tsc -noEmit" }, + "dependencies": { + "@spade-proxy/core": "*" + }, "devDependencies": { "@sentry/serverless": "7.52.1", "@types/aws-lambda": "^8.10.115", diff --git a/packages/functions/src/api/ucan-invocation-router.ts b/packages/functions/src/api/ucan-invocation-router.ts new file mode 100644 index 0000000..4ebcd23 --- /dev/null +++ b/packages/functions/src/api/ucan-invocation-router.ts @@ -0,0 +1,47 @@ +import { Config } from 'sst/node/config' +import { APIGatewayProxyHandlerV2, APIGatewayProxyEventV2 } from 'aws-lambda' + +import { + createUcantoServer, + getServiceSigner +} from '@spade-proxy/core/service' +// TODO: sentry + +async function ucanInvocationRouter(request: APIGatewayProxyEventV2) { + const { + OFFER_BUCKET_NAME: offerBucketName = '', + SPADE_PROXY_DID + } = process.env + + if (!SPADE_PROXY_DID) { + return { + statusCode: 500, + } + } else if (request.body === undefined) { + return { + statusCode: 400, + } + } + + const { PRIVATE_KEY } = Config + const serviceSigner = getServiceSigner({ SPADE_PROXY_DID, PRIVATE_KEY }) + const server = await createUcantoServer(serviceSigner, {}) + + return { + statusCode: 200, + // TODO + } +} + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + return ucanInvocationRouter(event) +} + +// would be generated by sst, but requires `sst build` to be run, which calls out to aws; not great for CI +declare module 'sst/node/config' { + export interface SecretResources { + PRIVATE_KEY: { + value: string + } + } +} diff --git a/packages/functions/sst-env.d.ts b/packages/functions/sst-env.d.ts new file mode 100644 index 0000000..a9187e8 --- /dev/null +++ b/packages/functions/sst-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31b9b24..85eae2c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,14 +27,36 @@ importers: sst: 2.8.7_typescript@5.0.4 typescript: 5.0.4 + packages/core: + specifiers: + '@ipld/dag-ucan': 3.3.2 + '@types/node': ^18.16.3 + '@ucanto/interface': 7.1.0 + '@ucanto/principal': ^7.0.0 + sst: ^2.8.3 + typescript: ^5.0.4 + vitest: ^0.31.0 + dependencies: + '@ipld/dag-ucan': 3.3.2 + '@ucanto/interface': 7.1.0 + '@ucanto/principal': 7.0.0 + devDependencies: + '@types/node': 18.16.5 + sst: 2.8.7_typescript@5.0.4 + typescript: 5.0.4 + vitest: 0.31.0 + packages/functions: specifiers: '@sentry/serverless': 7.52.1 + '@spade-proxy/core': '*' '@types/aws-lambda': ^8.10.115 '@types/node': ^18.16.3 sst: ^2.8.3 typescript: ^5.0.4 vitest: ^0.31.0 + dependencies: + '@spade-proxy/core': link:../core devDependencies: '@sentry/serverless': 7.52.1 '@types/aws-lambda': 8.10.115 @@ -2165,6 +2187,30 @@ packages: - utf-8-validate dev: true + /@ipld/dag-cbor/9.0.0: + resolution: {integrity: sha512-zdsiSiYDEOIDW7mmWOYWC9gukjXO+F8wqxz/LfN7iSwTfIyipC8+UQrCbPupFMRb/33XQTZk8yl3My8vUQBRoA==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} + dependencies: + cborg: 1.10.1 + multiformats: 11.0.2 + dev: false + + /@ipld/dag-json/10.0.1: + resolution: {integrity: sha512-XE1Eqw3eNVrSfOhtqCM/gwCxEgYFBzkDlkwhEeMmMvhd0rLBfSyVzXbahZSlv97tiTPEIx5rt41gcFAda3W8zg==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} + dependencies: + cborg: 1.10.1 + multiformats: 11.0.2 + dev: false + + /@ipld/dag-ucan/3.3.2: + resolution: {integrity: sha512-EhuOrAfnudsVYIbzEIgi3itHAEo3WZNOt1VNPsYhxKBhOzDMeoTXh6/IHc7ZKBW1T2vDQHdgj4m1r64z6MssGA==} + dependencies: + '@ipld/dag-cbor': 9.0.0 + '@ipld/dag-json': 10.0.1 + multiformats: 11.0.2 + dev: false + /@jridgewell/gen-mapping/0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -2199,6 +2245,10 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true + /@noble/ed25519/1.7.3: + resolution: {integrity: sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==} + dev: false + /@peculiar/asn1-schema/2.3.6: resolution: {integrity: sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==} dependencies: @@ -2395,6 +2445,23 @@ packages: '@types/node': 20.1.7 dev: true + /@ucanto/interface/7.1.0: + resolution: {integrity: sha512-DMD0ybPZ7UUtBx9g3TR6mrYW6N2IMhzgQ2IRfYsoTlhnXOTfmllwogWKhUJobf/lBHmmpi2CW8gWXTBby/cN5A==} + dependencies: + '@ipld/dag-ucan': 3.3.2 + multiformats: 11.0.2 + dev: false + + /@ucanto/principal/7.0.0: + resolution: {integrity: sha512-VZuLDDQWpkkKR8+MHdnigOd5AlnToeb4vz1bjCircxSNMUvIi5O2UAUkSArgSPzqPUeriZW6vRbwGK2DMqQ4UQ==} + dependencies: + '@ipld/dag-ucan': 3.3.2 + '@noble/ed25519': 1.7.3 + '@ucanto/interface': 7.1.0 + multiformats: 11.0.2 + one-webcrypto: 1.0.3 + dev: false + /@vitest/expect/0.31.0: resolution: {integrity: sha512-Jlm8ZTyp6vMY9iz9Ny9a0BHnCG4fqBa8neCF6Pk/c/6vkUk49Ls6UBlgGAU82QnzzoaUs9E/mUhq/eq9uMOv/g==} dependencies: @@ -2972,6 +3039,11 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /cborg/1.10.1: + resolution: {integrity: sha512-et6Qm8MOUY2kCWa5GKk2MlBVoPjHv0hQBmlzI/Z7+5V3VJCeIkGehIB3vWknNsm2kOkAIs6wEKJFJo8luWQQ/w==} + hasBin: true + dev: false + /cdk-assets/2.72.1: resolution: {integrity: sha512-qxKgIBAdJhBJV23WAGLcQ4x89k/zmYhWeQeZW3TEbv//TZFIRWlgpkz6XnTzNIuluWghxMOvHym/LoRMpSaJcg==} engines: {node: '>= 14.15.0'} @@ -4720,6 +4792,11 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true + /multiformats/11.0.2: + resolution: {integrity: sha512-b5mYMkOkARIuVZCpvijFj9a6m5wMVLC7cf/jIPd5D/ARDOfLC5+IFkbgDXQgcU2goIsTD/O9NY4DI/Mt4OGvlg==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} + dev: false + /mute-stream/0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} dev: true @@ -4811,6 +4888,10 @@ packages: wrappy: 1.0.2 dev: true + /one-webcrypto/1.0.3: + resolution: {integrity: sha512-fu9ywBVBPx0gS9K0etIROTiCkvI5S1TDjFsYFb3rC1ewFxeOqsbzq7aIMBHsYfrTHBcGXJaONXXjTl8B01cW1Q==} + dev: false + /onetime/5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} diff --git a/sst.config.ts b/sst.config.ts index 09ec499..3e1ccd1 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -1,6 +1,7 @@ import { Tags } from 'aws-cdk-lib'; import { SSTConfig } from 'sst'; import { ApiStack } from './stacks/ApiStack'; +import { DataStack } from './stacks/DataStack'; export default { config(_input) { @@ -20,6 +21,7 @@ export default { sourcemap: true } }) + app.stack(DataStack); app.stack(ApiStack); // tags let us discover all the aws resource costs incurred by this app diff --git a/stacks/ApiStack.ts b/stacks/ApiStack.ts index 3212c00..270d304 100644 --- a/stacks/ApiStack.ts +++ b/stacks/ApiStack.ts @@ -1,8 +1,11 @@ import { Api, + Config, StackContext, + use } from 'sst/constructs'; +import { DataStack } from './DataStack'; import { getApiPackageJson, getCustomDomain, @@ -11,6 +14,8 @@ import { } from './config'; export function ApiStack({ app, stack }: StackContext) { + const { offerBucket } = use(DataStack) + // Setup app monitoring with Sentry setupSentry(app, stack) @@ -18,20 +23,29 @@ export function ApiStack({ app, stack }: StackContext) { const customDomain = getCustomDomain(stack.stage, process.env.HOSTED_ZONE) const pkg = getApiPackageJson() const git = getGitInfo() + const privateKey = new Config.Secret(stack, 'PRIVATE_KEY') const api = new Api(stack, 'api', { customDomain, defaults: { function: { + permissions: [ + offerBucket + ], environment: { + OFFER_BUCKET_NAME: offerBucket.bucketName, NAME: pkg.name, VERSION: pkg.version, COMMIT: git.commmit, STAGE: stack.stage, - } + }, + bind: [ + privateKey + ] } }, routes: { + 'POST /': 'packages/functions/src/api/ucan-invocation-router.handler', 'GET /version': 'packages/functions/src/api/version.handler', 'GET /error': 'packages/functions/src/api/error.handler', } diff --git a/stacks/DataStack.ts b/stacks/DataStack.ts new file mode 100644 index 0000000..e1c8091 --- /dev/null +++ b/stacks/DataStack.ts @@ -0,0 +1,17 @@ +import { Bucket, StackContext } from 'sst/constructs'; + +import { getBucketConfig } from './config'; + +export function DataStack({ stack }: StackContext) { + const bucket = getBucketConfig('offer-store', stack.stage) + const offerBucket = new Bucket(stack, bucket.bucketName, { + cors: true, + cdk: { + bucket + } + }) + + return { + offerBucket + } +}