Skip to content

Commit

Permalink
feat: hono
Browse files Browse the repository at this point in the history
  • Loading branch information
whilefoo committed Dec 2, 2024
1 parent 84e4c7f commit fef7870
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 146 deletions.
22 changes: 20 additions & 2 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,26 @@
"ignorePaths": ["**/*.json", "**/*.css", "node_modules", "**/*.log"],
"useGitignore": true,
"language": "en",
"words": ["dataurl", "devpool", "fkey", "mswjs", "outdir", "servedir", "supabase", "typebox", "ubiquity-os",
"smee", "tomlify", "hono", "cfworker", "pavlovcik", "ghijklmnopqrstuvwxyz", "GHIJKLMNOPQRSTUVWXYZ", "ubiquityos"],
"words": [
"dataurl",
"devpool",
"fkey",
"mswjs",
"outdir",
"servedir",
"supabase",
"typebox",
"ubiquity-os",
"smee",
"tomlify",
"hono",
"cfworker",
"workerd",
"pavlovcik",
"ghijklmnopqrstuvwxyz",
"GHIJKLMNOPQRSTUVWXYZ",
"ubiquityos"
],
"dictionaries": ["typescript", "node", "software-terms"],
"import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"],
"ignoreRegExpList": ["[0-9a-fA-F]{6}"]
Expand Down
2 changes: 1 addition & 1 deletion .github/knip.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { KnipConfig } from "knip";

const config: KnipConfig = {
entry: ["src/worker.ts", "deploy/setup-kv-namespace.ts"],
entry: ["src/kernel.ts", "src/cloudflare-worker.ts", "deploy/setup-kv-namespace.ts"],
project: ["src/**/*.ts"],
ignore: ["jest.config.ts"],
ignoreBinaries: ["i", "publish"],
Expand Down
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default tsEslint.config({
"@typescript-eslint": tsEslint.plugin,
"check-file": checkFile,
},
ignores: [".github/knip.ts", "**/.wrangler/**", "jest.config.ts", ".husky/**"],
ignores: [".github/knip.ts", "**/.wrangler/**", "jest.config.ts", ".husky/**", "dist/**"],
extends: [eslint.configs.recommended, ...tsEslint.configs.recommended, sonarjs.configs.recommended],
languageOptions: {
parser: tsEslint.parser,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@sinclair/typebox": "0.34.3",
"@ubiquity-os/plugin-sdk": "^1.1.0",
"dotenv": "16.4.5",
"hono": "^4.6.12",
"openai": "^4.70.2",
"typebox-validators": "0.3.5",
"yaml": "2.4.5"
Expand Down
3 changes: 3 additions & 0 deletions src/adapters/cloudflare-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { app } from "../kernel";

export default app;
94 changes: 94 additions & 0 deletions src/kernel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { emitterEventNames } from "@octokit/webhooks";
import { Value } from "@sinclair/typebox/value";
import { GitHubEventHandler } from "./github/github-event-handler";
import { bindHandlers } from "./github/handlers";
import { Env, envSchema } from "./github/types/env";
import { EmptyStore } from "./github/utils/kv-store";
import { WebhookEventName } from "@octokit/webhooks-types";
import OpenAI from "openai";
import { Context, Hono, HonoRequest } from "hono";
import { env as honoEnv, getRuntimeKey } from "hono/adapter";
import { StatusCode } from "hono/utils/http-status";

export const app = new Hono();

app.post("/", async (ctx: Context) => {
try {
const env = honoEnv(ctx);
const request = ctx.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);

// if running in Cloudflare Worker, handle the webhook in the background and return a response immediately
if (getRuntimeKey() === "workerd") {
const waitUntil = ctx.executionCtx.waitUntil;
waitUntil(eventHandler.webhooks.verifyAndReceive({ id, name: eventName, payload: await request.text(), signature: signatureSha256 }));
} else {
await eventHandler.webhooks.verifyAndReceive({ id, name: eventName, payload: await request.text(), signature: signatureSha256 });
}
return ctx.text("ok\n", 200);
} catch (error) {
return handleUncaughtError(ctx, error);
}
});

function handleUncaughtError(ctx: Context, 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" && typeof err.status === "number" ? err.status : 500;
} else {
errorMessage = error instanceof Error ? `${error.name}: ${error.message}` : `Error: ${error}`;
}
return ctx.json({ error: errorMessage }, status as StatusCode);
}

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");
}
}

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;
}

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;
}

function getId(request: HonoRequest): string {
const id = request.header("x-github-delivery");
if (!id) {
throw new Error(`Missing "x-github-delivery" header`);
}
return id;
}
81 changes: 0 additions & 81 deletions src/worker.ts

This file was deleted.

46 changes: 12 additions & 34 deletions tests/dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,16 @@ describe("handleEvent", () => {
const payloadString = JSON.stringify(payload);
const signature = calculateSignature(payloadString, secret);

const req = new Request("http://localhost:8080", {
process.env = {
ENVIRONMENT: "production",
APP_WEBHOOK_SECRET: secret,
APP_ID: "1",
APP_PRIVATE_KEY: "1234",
OPENAI_API_KEY: "token",
};

const app = (await import("../src/kernel")).app;
const res = await app.request("http://localhost:8080", {
method: "POST",
headers: {
"x-github-event": "issue_comment.created",
Expand All @@ -153,39 +162,8 @@ describe("handleEvent", () => {
body: payloadString,
});

const worker = (await import("../src/worker")).default;
// I didn't find a better option to make sure that the non-awaited ctx.waitUntil is resolved
// eslint-disable-next-line no-async-promise-executor
const waitUntilPromise = new Promise<void>(async (resolve) => {
const res = await worker.fetch(
req,
{
ENVIRONMENT: "production",
APP_WEBHOOK_SECRET: secret,
APP_ID: "1",
APP_PRIVATE_KEY: "1234",
PLUGIN_CHAIN_STATE: {} as KVNamespace,
OPENAI_API_KEY: "token",
},
{
waitUntil<T>(promise: Promise<T>) {
promise
.then(() => {
resolve();
})
.catch(() => {
resolve();
});
},
passThroughOnException() {},
}
);

expect(res).toBeTruthy();
});

await waitUntilPromise;

expect(res).toBeTruthy();
// 2 calls means the execution didn't break
expect(dispatchWorker).toHaveBeenCalledTimes(2);

dispatchWorker.mockReset();
Expand Down
29 changes: 12 additions & 17 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { http, HttpResponse } from "msw";
import { GitHubContext } from "../src/github/github-context";
import { GitHubEventHandler } from "../src/github/github-event-handler";
import { getConfig } from "../src/github/utils/config";
import worker from "../src/worker"; // has to be imported after the mocks
import { app } from "../src/kernel"; // has to be imported after the mocks
import { server } from "./__mocks__/node";
import "./__mocks__/webhooks";

Expand Down Expand Up @@ -59,24 +59,19 @@ describe("Worker tests", () => {
);
});
it("Should fail on missing env variables", async () => {
const req = new Request("http://localhost:8080");
const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => jest.fn());
const res = await worker.fetch(
req,
{
ENVIRONMENT: "production",
APP_WEBHOOK_SECRET: "",
APP_ID: "",
APP_PRIVATE_KEY: "",
PLUGIN_CHAIN_STATE: {} as KVNamespace,
OPENAI_API_KEY: "token",
},
{
waitUntil: function () {},
passThroughOnException: function () {},
}
);
process.env = {
ENVIRONMENT: "production",
APP_WEBHOOK_SECRET: "",
APP_ID: "",
APP_PRIVATE_KEY: "",
OPENAI_API_KEY: "token",
};
const res = await app.request("http://localhost:8080", {
method: "POST",
});
expect(res.status).toEqual(500);
expect(await res.json()).toEqual({ error: "Error: Invalid environment variables" });
consoleSpy.mockReset();
});

Expand Down
20 changes: 10 additions & 10 deletions wrangler.toml
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
name = "ubiquity-os-kernel"
main = "src/worker.ts"
main = "src/adapters/cloudflare-worker.ts"
compatibility_date = "2023-12-06"
compatibility_flags = [ "nodejs_compat" ]
compatibility_flags = ["nodejs_compat"]

# Prefer this syntax due to a bug in Wrangler: https://github.com/cloudflare/workers-sdk/issues/5634
[env]
[env.dev]
[[env.dev.kv_namespaces]]
binding = "PLUGIN_CHAIN_STATE"
id = "4f7aadc56bef41a7ae2cc8c0582320b3"
[env.dev]
[[env.dev.kv_namespaces]]
binding = "PLUGIN_CHAIN_STATE"
id = "4f7aadc56bef41a7ae2cc8c0582320b3"

[env.production]
[[env.production.kv_namespaces]]
binding = "PLUGIN_CHAIN_STATE"
id = "TO_BE_DEFINED"
[env.production]
[[env.production.kv_namespaces]]
binding = "PLUGIN_CHAIN_STATE"
id = "TO_BE_DEFINED"

# Enables Cloudflare Worker Logs
[observability]
Expand Down

0 comments on commit fef7870

Please sign in to comment.