Skip to content

Commit

Permalink
feat: sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
whilefoo committed Jun 20, 2024
1 parent fc9a358 commit b42f9b6
Show file tree
Hide file tree
Showing 13 changed files with 512 additions and 10 deletions.
Binary file modified bun.lockb
Binary file not shown.
10 changes: 10 additions & 0 deletions jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"preset": "ts-jest",
"testEnvironment": "node",
"roots": ["./tests"],
"coveragePathIgnorePatterns": ["node_modules", "mocks"],
"collectCoverage": true,
"coverageReporters": ["json", "lcov", "text", "clover", "json-summary"],
"reporters": ["default", "jest-junit"],
"coverageDirectory": "coverage"
}
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"proxy": "tsx src/proxy.ts",
"knip": "knip --config .github/knip.ts",
"knip-ci": "knip --no-exit-code --reporter json --config .github/knip.ts",
"test": "bun test"
"test": "jest test"
},
"keywords": [
"typescript",
Expand All @@ -44,10 +44,16 @@
"@octokit/webhooks-types": "^7.5.1",
"@sinclair/typebox": "^0.32.5",
"dotenv": "^16.4.4",
"hono": "^4.4.7",
"smee-client": "^2.0.0",
"yaml": "^2.4.1"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"jest-junit": "^16.0.0",
"ts-jest": "^29.1.5",
"@cloudflare/workers-types": "^4.20240117.0",
"@commitlint/cli": "^18.6.1",
"@commitlint/config-conventional": "^18.6.2",
Expand Down
4 changes: 2 additions & 2 deletions src/github/github-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Octokit } from "@octokit/core";
import { RequestOptions } from "@octokit/types";
import { paginateRest } from "@octokit/plugin-paginate-rest";
import { legacyRestEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
import { restEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
import { retry } from "@octokit/plugin-retry";
import { throttling } from "@octokit/plugin-throttling";
import { createAppAuth } from "@octokit/auth-app";
Expand Down Expand Up @@ -46,6 +46,6 @@ function requestLogging(octokit: Octokit) {
});
}

export const customOctokit = Octokit.plugin(throttling, retry, paginateRest, legacyRestEndpointMethods, requestLogging).defaults((instanceOptions: object) => {
export const customOctokit = Octokit.plugin(throttling, retry, paginateRest, restEndpointMethods, requestLogging).defaults((instanceOptions: object) => {
return Object.assign({}, defaultOptions, instanceOptions);
});
1 change: 1 addition & 0 deletions src/sdk/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const UBIQUIBOT_KERNEL_PUBLIC_KEY = "";
19 changes: 19 additions & 0 deletions src/sdk/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { EmitterWebhookEvent as WebhookEvent, EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks";
import { Octokit } from "@octokit/rest";

export interface Context<TConfig = unknown, TEnv = unknown, TSupportedEvents extends WebhookEventName = WebhookEventName> {
eventName: TSupportedEvents;
payload: {
[K in TSupportedEvents]: K extends WebhookEventName ? WebhookEvent<K> : never;
}[TSupportedEvents]["payload"];
octokit: InstanceType<typeof Octokit>;
config: TConfig;
env: TEnv;
logger: {
fatal: (message: unknown, ...optionalParams: unknown[]) => void;
error: (message: unknown, ...optionalParams: unknown[]) => void;
warn: (message: unknown, ...optionalParams: unknown[]) => void;
info: (message: unknown, ...optionalParams: unknown[]) => void;
debug: (message: unknown, ...optionalParams: unknown[]) => void;
};
}
1 change: 1 addition & 0 deletions src/sdk/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createPlugin } from "./server";
65 changes: 65 additions & 0 deletions src/sdk/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Hono } from "hono";
import { HTTPException } from "hono/http-exception";
import { Context } from "./context";
import { customOctokit } from "../github/github-client";
import { EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks";
import { verifySignature } from "./signature";
import { UBIQUIBOT_KERNEL_PUBLIC_KEY } from "./constants";

interface Options {
ubiquibotKernelPublicKey?: string;
logger?: {
fatal?: (message: unknown, ...optionalParams: unknown[]) => void;
error?: (message: unknown, ...optionalParams: unknown[]) => void;
warn?: (message: unknown, ...optionalParams: unknown[]) => void;
info?: (message: unknown, ...optionalParams: unknown[]) => void;
debug?: (message: unknown, ...optionalParams: unknown[]) => void;
};
}

export async function createPlugin<TConfig = unknown, TEnv = unknown, TSupportedEvents extends WebhookEventName = WebhookEventName>(
handler: (context: Context<TConfig, TEnv, TSupportedEvents>) => Promise<Record<string, unknown> | undefined>,
options?: Options
) {
const app = new Hono();

app.post("/", async (ctx) => {
if (ctx.req.header("content-type") !== "application/json") {
throw new HTTPException(400, { message: "Content-Type must be application/json" });
}

const payload = await ctx.req.json();
const signature = payload.signature;
delete payload.signature;
if (!(await verifySignature(options?.ubiquibotKernelPublicKey || UBIQUIBOT_KERNEL_PUBLIC_KEY, payload, signature))) {
console.error("Invalid signature");
throw new HTTPException(400, { message: "Invalid signature" });
}

const context: Context<TConfig, TEnv, TSupportedEvents> = {
eventName: payload.eventName,
payload: payload.payload,
octokit: new customOctokit({ auth: payload.authToken }),
config: payload.settings as TConfig,
env: ctx.env as TEnv,
logger: {
fatal: options?.logger?.fatal || console.error,
error: options?.logger?.error || console.error,
warn: options?.logger?.warn || console.warn,
info: options?.logger?.info || console.info,
debug: options?.logger?.debug || console.debug,
},
};

try {
console.log("CALLING HANDLER");
const result = await handler(context);
return ctx.json({ stateId: payload.stateId, output: result });
} catch (error) {
console.error(error);
throw new HTTPException(500, { message: "Unexpected error" });
}
});

return app;
}
20 changes: 20 additions & 0 deletions src/sdk/signature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export async function verifySignature(publicKeyPem: string, payload: unknown, signature: string) {
const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").trim();
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));

const publicKey = await crypto.subtle.importKey(
"spki",
binaryDer.buffer,
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
},
true,
["verify"]
);

const signatureArray = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0));
const dataArray = new TextEncoder().encode(JSON.stringify(payload));

return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", publicKey, signatureArray, dataArray);
}
Loading

0 comments on commit b42f9b6

Please sign in to comment.