diff --git a/bun.lockb b/bun.lockb index 949ad02..9fd3a10 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index d1bba65..e4f1789 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,7 @@ "version": "2.1.5", "private": false, "description": "The kernel for UbiquityOS.", - "module": "dist/index.mjs", - "main": "dist/src/functions/*.js", + "main": "dist/*.js", "typings": "dist/index.d.ts", "files": [ "dist" @@ -14,6 +13,7 @@ "engines": { "node": ">=20.10.0" }, + "type": "module", "scripts": { "dev": "run-p worker proxy", "predev": "tsx predev.ts", @@ -35,8 +35,8 @@ "setup": "tsx ./scripts/setup.ts", "start": "func start", "prestart": "bun run build", - "build": "tsc", - "prebuild": "rimraf dist" + "build": "tsup", + "prebuild": "rimraf dist" }, "keywords": [ "typescript", @@ -91,6 +91,7 @@ "eslint-plugin-check-file": "2.8.0", "eslint-plugin-prettier": "5.1.3", "eslint-plugin-sonarjs": "1.0.3", + "glob": "^11.0.0", "husky": "9.1.6", "jest": "29.7.0", "jest-junit": "16.0.0", @@ -106,6 +107,7 @@ "toml": "3.0.0", "tomlify-j0.4": "3.0.0", "ts-node": "^10.9.2", + "tsup": "^8.3.5", "tsx": "4.16.2", "typescript": "5.6.3", "typescript-eslint": "7.16.0", diff --git a/src/app.ts b/src/app.ts index fd5f1ca..f3bfa65 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,91 @@ -import { Hono } from "hono"; +import { Hono, HonoRequest } from "hono"; +import { Env, envSchema } from "./github/types/env"; +import { Value } from "@sinclair/typebox/value"; +import { WebhookEventName } from "@octokit/webhooks-types"; +import { emitterEventNames } from "@octokit/webhooks"; +import OpenAI from "openai"; +import { GitHubEventHandler } from "./github/github-event-handler"; +import { EmptyStore } from "./github/utils/kv-store"; +import { bindHandlers } from "./github/handlers"; const app = new Hono(); -app.get("/", (c) => c.text("Hello Azure Functions!")); +function validateEnv(env: Env): void { + if (!Value.Check(envSchema, env)) { + const errors = [...Value.Errors(envSchema, env)]; + console.error("Invalid environment variables", errors); + throw new Error("Invalid environment variables"); + } +} + +export function getEventName(request: HonoRequest): WebhookEventName { + const eventName = request.header("x-github-event"); + if (!eventName || !emitterEventNames.includes(eventName as WebhookEventName)) { + throw new Error(`Unsupported or missing "x-github-event" header value: ${eventName}`); + } + return eventName as WebhookEventName; +} + +export function getSignature(request: HonoRequest): string { + const signatureSha256 = request.header("x-hub-signature-256"); + if (!signatureSha256) { + throw new Error(`Missing "x-hub-signature-256" header`); + } + return signatureSha256; +} + +export function getId(request: HonoRequest): string { + const id = request.header("x-github-delivery"); + if (!id) { + throw new Error(`Missing "x-github-delivery" header`); + } + return id; +} + +function handleUncaughtError(error: unknown) { + console.error(error); + let status = 500; + let errorMessage = "An uncaught error occurred"; + if (error instanceof AggregateError) { + const err = error.errors[0]; + errorMessage = err.message ? `${err.name}: ${err.message}` : `Error: ${errorMessage}`; + status = typeof err.status !== "undefined" ? err.status : 500; + } else { + errorMessage = error instanceof Error ? `${error.name}: ${error.message}` : `Error: ${error}`; + } + return new Response(JSON.stringify({ error: errorMessage }), { status: status, headers: { "content-type": "application/json" } }); +} + +app.get("/", async (c) => { + try { + const env = c.env as Env; + const request = c.req; + validateEnv(env); + const eventName = getEventName(request); + const signatureSha256 = getSignature(request); + const id = getId(request); + const openAiClient = new OpenAI({ + apiKey: env.OPENAI_API_KEY, + }); + const eventHandler = new GitHubEventHandler({ + environment: env.ENVIRONMENT, + webhookSecret: env.APP_WEBHOOK_SECRET, + appId: env.APP_ID, + privateKey: env.APP_PRIVATE_KEY, + pluginChainState: new EmptyStore(), + openAiClient, + }); + bindHandlers(eventHandler); + await eventHandler.webhooks.verifyAndReceive({ + id, + name: eventName, + payload: await request.text(), + signature: signatureSha256, + }); + } catch (error) { + return handleUncaughtError(error); + } + + return c.text("OK"); +}); export default app; diff --git a/src/worker.ts b/src/worker.ts index de266d6..b34b9bf 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -48,7 +48,7 @@ function handleUncaughtError(error: unknown) { return new Response(JSON.stringify({ error: errorMessage }), { status: status, headers: { "content-type": "application/json" } }); } -function validateEnv(env: Env): void { +export function validateEnv(env: Env): void { if (!Value.Check(envSchema, env)) { const errors = [...Value.Errors(envSchema, env)]; console.error("Invalid environment variables", errors); @@ -56,7 +56,7 @@ function validateEnv(env: Env): void { } } -function getEventName(request: Request): WebhookEventName { +export function getEventName(request: Request): WebhookEventName { const eventName = request.headers.get("x-github-event"); if (!eventName || !emitterEventNames.includes(eventName as WebhookEventName)) { throw new Error(`Unsupported or missing "x-github-event" header value: ${eventName}`); @@ -64,7 +64,7 @@ function getEventName(request: Request): WebhookEventName { return eventName as WebhookEventName; } -function getSignature(request: Request): string { +export function getSignature(request: Request): string { const signatureSha256 = request.headers.get("x-hub-signature-256"); if (!signatureSha256) { throw new Error(`Missing "x-hub-signature-256" header`); @@ -72,7 +72,7 @@ function getSignature(request: Request): string { return signatureSha256; } -function getId(request: Request): string { +export function getId(request: Request): string { const id = request.headers.get("x-github-delivery"); if (!id) { throw new Error(`Missing "x-github-delivery" header`); diff --git a/tsconfig.json b/tsconfig.json index ee8d923..2edb943 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,8 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": ["ES2022"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, // "jsx": "react" /* Specify what JSX code is generated. */, // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ @@ -24,9 +24,9 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ /* Modules */ - "module": "commonjs" /* Specify what module code is generated. */, + "module": "ES2022" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + "moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ @@ -68,7 +68,7 @@ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ /* Interop Constraints */ - "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, + //"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..1f1a06d --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/functions/*.ts"], + splitting: false, + sourcemap: false, + clean: true, + format: ["esm"], + minify: true, +});