Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge develop into main #82

Merged
merged 26 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"ignorePaths": ["**/*.json", "**/*.css", "node_modules", "**/*.log"],
"useGitignore": true,
"language": "en",
"words": ["dataurl", "devpool", "fkey", "mswjs", "outdir", "servedir", "supabase", "typebox", "ubiquibot", "smee", "tomlify"],
"words": ["dataurl", "devpool", "fkey", "mswjs", "outdir", "servedir", "supabase", "typebox", "ubiquibot", "smee", "tomlify", "hono"],
"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
5 changes: 3 additions & 2 deletions .github/knip.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { KnipConfig } from "knip";

const config: KnipConfig = {
entry: ["src/worker.ts"],
entry: ["src/worker.ts", "deploy/setup-kv-namespace.ts"],
project: ["src/**/*.ts"],
ignore: ["jest.config.ts"],
ignoreBinaries: ["i"],
ignoreExportsUsedInFile: true,
ignoreDependencies: ["@mswjs/data", "esbuild", "eslint-config-prettier", "eslint-plugin-prettier", "msw"],
ignoreDependencies: ["@mswjs/data", "esbuild", "eslint-config-prettier", "eslint-plugin-prettier", "msw", "ts-node"],
0x4007 marked this conversation as resolved.
Show resolved Hide resolved
};

export default config;
Binary file modified bun.lockb
100644 → 100755
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/**"],
ignores: [".github/knip.ts", "**/.wrangler/**", "jest.config.ts"],
extends: [eslint.configs.recommended, ...tsEslint.configs.recommended, sonarjs.configs.recommended],
languageOptions: {
parser: tsEslint.parser,
Expand Down
11 changes: 0 additions & 11 deletions jest.config.json

This file was deleted.

21 changes: 21 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Config } from "jest";

const config: Config = {
testEnvironment: "node",
roots: ["./tests"],
coveragePathIgnorePatterns: ["node_modules", "mocks"],
collectCoverage: true,
coverageReporters: ["json", "lcov", "text", "clover", "json-summary"],
reporters: ["default", "jest-junit"],
coverageDirectory: "coverage",
verbose: true,
transformIgnorePatterns: [],
transform: {
"^.+\\.[j|t]s$": "@swc/jest",
},
moduleNameMapper: {
"@octokit/webhooks-methods": "<rootDir>/node_modules/@octokit/webhooks-methods/dist-node/index.js",
},
};

export default config;
40 changes: 29 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
{
"name": "ubiquibot-kernel",
"version": "0.0.0",
"name": "@ubiquity-dao/ubiquibot-kernel",
"version": "0.0.1",
"private": false,
"description": "The kernel for UbiquiBot.",
"main": "src/worker.ts",
"module": "dist/esm/index.js",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/esm/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist"
],
"author": "Ubiquity DAO",
"license": "MIT",
"engines": {
Expand All @@ -25,7 +37,8 @@
"knip-ci": "knip --no-exit-code --reporter json --config .github/knip.ts",
"jest:test": "jest --coverage",
"plugin:hello-world": "tsx tests/__mocks__/hello-world-plugin.ts",
"setup-kv": "bun --env-file=.dev.vars deploy/setup-kv-namespace.ts"
"setup-kv": "bun --env-file=.dev.vars deploy/setup-kv-namespace.ts",
"sdk:build": "tsup"
},
"keywords": [
"typescript",
Expand All @@ -44,22 +57,30 @@
"@octokit/types": "13.5.0",
"@octokit/webhooks": "13.2.8",
"@octokit/webhooks-types": "7.5.1",
"@sinclair/typebox": "0.32.34",
"@sinclair/typebox": "0.32.35",
"@ubiquity-dao/ubiquibot-logger": "1.3.0",
"dotenv": "16.4.5",
"jest": "29.7.0",
"hono": "4.4.13",
"smee-client": "2.0.1",
"ts-node": "^10.9.2",
0x4007 marked this conversation as resolved.
Show resolved Hide resolved
"typebox-validators": "0.3.5",
"yaml": "2.4.5"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240117.0",
"@swc/core": "1.6.5",
"@swc/jest": "0.2.36",
"tsup": "8.1.0",
"@jest/globals": "29.7.0",
"@types/jest": "29.5.12",
"jest": "29.7.0",
"jest-junit": "16.0.0",
"@cloudflare/workers-types": "4.20240117.0",
"@commitlint/cli": "19.3.0",
"@commitlint/config-conventional": "19.2.2",
"@cspell/dict-node": "5.0.1",
"@cspell/dict-software-terms": "3.4.6",
"@cspell/dict-typescript": "3.1.5",
"@eslint/js": "9.7.0",
"@jest/globals": "29.7.0",
"@mswjs/data": "0.16.1",
"@mswjs/http-middleware": "0.10.1",
"@types/node": "20.14.10",
Expand All @@ -71,15 +92,12 @@
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-sonarjs": "1.0.3",
"husky": "9.0.11",
"jest-junit": "16.0.0",
"jest-md-dashboard": "0.8.0",
"knip": "5.26.0",
"lint-staged": "15.2.7",
"npm-run-all": "4.1.5",
"prettier": "3.3.3",
"toml": "3.0.0",
"tomlify-j0.4": "3.0.0",
"ts-jest": "29.2.2",
"tsx": "4.16.2",
"typescript": "5.5.3",
"typescript-eslint": "7.16.0",
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);
});
2 changes: 1 addition & 1 deletion src/github/handlers/help-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function postHelpCommand(context: GitHubContext<"issue_comment.crea
const { plugin } = pluginElement.uses[0];
commands.push(...(await parseCommandsFromManifest(context, plugin)));
}
await context.octokit.issues.createComment({
await context.octokit.rest.issues.createComment({
body: comments.concat(commands.sort()).join("\n"),
issue_number: context.payload.issue.number,
owner: context.payload.repository.owner.login,
Expand Down
1 change: 1 addition & 0 deletions src/github/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ async function shouldSkipPlugin(event: EmitterWebhookEvent, context: GitHubConte
if (
context.key === "issue_comment.created" &&
manifest &&
manifest.commands &&
!Object.keys(manifest.commands).some(
(command) => "comment" in context.payload && typeof context.payload.comment !== "string" && context.payload.comment?.body.startsWith(`/${command}`)
)
Expand Down
8 changes: 4 additions & 4 deletions src/github/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { expressionRegex } from "../types/plugin";
import { configSchema, configSchemaValidator, PluginConfiguration } from "../types/plugin-configuration";
import { getManifest } from "./plugins";

const UBIQUIBOT_CONFIG_FULL_PATH = ".github/.ubiquibot-config.yml";
const UBIQUIBOT_CONFIG_ORG_REPO = "ubiquibot-config";
export const CONFIG_FULL_PATH = ".github/.ubiquibot-config.yml";
export const CONFIG_ORG_REPO = "ubiquibot-config";

async function getConfigurationFromRepo(context: GitHubContext, repository: string, owner: string) {
const targetRepoConfiguration: PluginConfiguration = parseYaml(
Expand Down Expand Up @@ -60,7 +60,7 @@ export async function getConfig(context: GitHubContext): Promise<PluginConfigura
let mergedConfiguration: PluginConfiguration = defaultConfiguration;

const configurations = await Promise.all([
getConfigurationFromRepo(context, UBIQUIBOT_CONFIG_ORG_REPO, payload.repository.owner.login),
getConfigurationFromRepo(context, CONFIG_ORG_REPO, payload.repository.owner.login),
getConfigurationFromRepo(context, payload.repository.name, payload.repository.owner.login),
]);

Expand Down Expand Up @@ -143,7 +143,7 @@ async function download({ context, repository, owner }: { context: GitHubContext
const { data } = await context.octokit.rest.repos.getContent({
owner,
repo: repository,
path: UBIQUIBOT_CONFIG_FULL_PATH,
path: CONFIG_FULL_PATH,
mediaType: { format: "raw" },
});
return data as unknown as string; // this will be a string if media format is raw
Expand Down
2 changes: 1 addition & 1 deletion src/github/utils/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async function fetchActionManifest(context: GitHubContext<"issue_comment.created
return _manifestCache[manifestKey];
}
try {
const { data } = await context.octokit.repos.getContent({
const { data } = await context.octokit.rest.repos.getContent({
owner,
repo,
path: "manifest.json",
Expand Down
6 changes: 3 additions & 3 deletions src/github/utils/workflow-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface WorkflowDispatchOptions {
}

async function getInstallationOctokitForOrg(context: GitHubContext, owner: string): Promise<InstanceType<typeof customOctokit>> {
const installations = await context.octokit.apps.listInstallations();
const installations = await context.octokit.rest.apps.listInstallations();
const installation = installations.data.find((inst) => inst.account?.login === owner);

if (!installation) {
Expand All @@ -23,7 +23,7 @@ async function getInstallationOctokitForOrg(context: GitHubContext, owner: strin
export async function dispatchWorkflow(context: GitHubContext, options: WorkflowDispatchOptions) {
const authenticatedOctokit = await getInstallationOctokitForOrg(context, options.owner);

return await authenticatedOctokit.actions.createWorkflowDispatch({
return await authenticatedOctokit.rest.actions.createWorkflowDispatch({
owner: options.owner,
repo: options.repository,
workflow_id: options.workflowId,
Expand All @@ -45,7 +45,7 @@ export async function dispatchWorker(targetUrl: string, payload?: Record<string,

export async function getDefaultBranch(context: GitHubContext, owner: string, repository: string) {
const octokit = await getInstallationOctokitForOrg(context, owner); // we cannot access other repos with the context's octokit
const repo = await octokit.repos.get({
const repo = await octokit.rest.repos.get({
owner: owner,
repo: repository,
});
Expand Down
10 changes: 10 additions & 0 deletions src/sdk/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const KERNEL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo1AYSyrXTEjMj2USno6i
5dv9Da386RI/zGypmkWa1KrIspE/Yd8RPaaEVAwt7p6+YeGcOQVLruuk11fJxff1
xB+KGbk1+kIdQ7s70B7yVRZuIM/k5aGfPpeerm0wjt4dKcYTRrl/OjLOMRrZ3vCX
E96v6eHEOpZIJ9VnjzGA0xymc+kBuEmZKabuK16S9a7I+CkZC8unqXWi15Chlyw8
EmbquwSmXI8VJ4teUjsF/H1MhJK3WTfsdr8bDsooRTMVKVBL8jONpGPzJZtr39zY
dkRj2Je2kag9b3FMxskv1npNSrPVcSc5lGNYlnZnfxIAnCknOC118JjitlrpT6wd
8wIDAQAB
-----END PUBLIC KEY-----
`;
14 changes: 14 additions & 0 deletions src/sdk/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { EmitterWebhookEvent as WebhookEvent, EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks";
import { customOctokit } from "./octokit";
import { Logs } from "@ubiquity-dao/ubiquibot-logger";

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 customOctokit>;
config: TConfig;
env: TEnv;
logger: Logs;
}
2 changes: 2 additions & 0 deletions src/sdk/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { createPlugin } from "./server";
export type { Context } from "./context";
27 changes: 27 additions & 0 deletions src/sdk/octokit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Octokit } from "@octokit/core";
import { RequestOptions } from "@octokit/types";
import { paginateRest } from "@octokit/plugin-paginate-rest";
import { restEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
import { retry } from "@octokit/plugin-retry";
import { throttling } from "@octokit/plugin-throttling";

const defaultOptions = {
throttle: {
onAbuseLimit: (retryAfter: number, options: RequestOptions, octokit: Octokit) => {
octokit.log.warn(`Abuse limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`);
return true;
},
onRateLimit: (retryAfter: number, options: RequestOptions, octokit: Octokit) => {
octokit.log.warn(`Rate limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`);
return true;
},
onSecondaryRateLimit: (retryAfter: number, options: RequestOptions, octokit: Octokit) => {
octokit.log.warn(`Secondary rate limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`);
return true;
},
},
};

export const customOctokit = Octokit.plugin(throttling, retry, paginateRest, restEndpointMethods).defaults((instanceOptions: object) => {
return Object.assign({}, defaultOptions, instanceOptions);
});
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 "./octokit";
import { EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks";
import { verifySignature } from "./signature";
import { KERNEL_PUBLIC_KEY } from "./constants";
import { Logs, LogLevel, LOG_LEVEL } from "@ubiquity-dao/ubiquibot-logger";
import { Manifest } from "../types/manifest";

interface Options {
kernelPublicKey?: string;
logLevel?: LogLevel;
}

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

app.get("/manifest.json", (ctx) => {
return ctx.json(manifest);
});

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?.kernelPublicKey || KERNEL_PUBLIC_KEY, payload, signature))) {
throw new HTTPException(400, { message: "Invalid signature" });
}

try {
new customOctokit({ auth: payload.authToken });
} catch (error) {
console.error("SDK ERROR", error);
throw new HTTPException(500, { message: "Unexpected error" });
}

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: new Logs(options?.logLevel || LOG_LEVEL.INFO),
};

try {
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,
{
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
Loading