From 85a3bf4a98979442224a85ea96911693a8cd0d5a Mon Sep 17 00:00:00 2001 From: whilefoo Date: Fri, 3 Nov 2023 20:27:05 +0100 Subject: [PATCH] feat: new config --- package.json | 2 +- src/adapters/index.ts | 27 ++-- src/adapters/supabase/helpers/tables/logs.ts | 20 +-- .../supabase/helpers/tables/wallet.test.ts | 4 +- src/bindings/config.ts | 22 +-- src/bindings/env.ts | 10 ++ src/bindings/event.ts | 6 +- src/handlers/access/labels-access.ts | 4 +- src/handlers/assign/action.ts | 2 +- src/handlers/comment/action.ts | 6 +- .../assign/get-time-labels-assigned.ts | 2 +- src/handlers/comment/handlers/assign/index.ts | 14 +- src/handlers/comment/handlers/first.ts | 4 +- src/handlers/comment/handlers/help.ts | 4 +- src/handlers/comment/handlers/index.ts | 13 +- .../handlers/issue/calculate-quality-score.ts | 2 +- .../issue/generate-permit-2-signature.ts | 7 +- .../handlers/issue/generate-permits.ts | 7 +- .../comment/handlers/issue/issue-closed.ts | 2 +- src/handlers/comment/handlers/wallet.ts | 2 +- src/handlers/pricing/pre.ts | 9 +- src/handlers/pricing/pricing-label.ts | 14 +- src/handlers/push/index.ts | 17 +-- src/handlers/push/update-base-rate.ts | 2 +- src/handlers/shared/pricing.ts | 8 +- src/handlers/wildcard/analytics.ts | 9 +- src/handlers/wildcard/unassign/unassign.ts | 5 +- src/helpers/gpt.ts | 7 +- src/helpers/issue.ts | 3 +- src/helpers/label.ts | 6 +- src/helpers/shared.ts | 4 +- src/types/configuration-types.ts | 63 +++++--- src/types/index.ts | 2 + src/types/logs.ts | 9 ++ src/types/openai.ts | 19 +++ src/ubiquibot-config-default.ts | 2 +- src/utils/generate-configuration.ts | 135 +++++++++--------- src/utils/private.ts | 38 +++++ yarn.lock | 8 +- 39 files changed, 303 insertions(+), 217 deletions(-) create mode 100644 src/bindings/env.ts create mode 100644 src/types/logs.ts create mode 100644 src/types/openai.ts create mode 100644 src/utils/private.ts diff --git a/package.json b/package.json index 2b15fdc68..a8b41f6fa 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@octokit/rest": "^20.0.2", "@openzeppelin/contracts": "^5.0.0", "@probot/adapter-github-actions": "^3.1.3", - "@sinclair/typebox": "^0.31.5", + "@sinclair/typebox": "^0.31.22", "@supabase/supabase-js": "^2.4.0", "@types/ms": "^0.7.31", "@types/parse5": "^7.0.0", diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 056b585f6..cbc569354 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -9,25 +9,22 @@ import { Super } from "./supabase/helpers/tables/super"; import { User } from "./supabase/helpers/tables/user"; import { Wallet } from "./supabase/helpers/tables/wallet"; import { Database } from "./supabase/types"; +import { env } from "../bindings/env"; + +const supabaseClient = createClient(env.SUPABASE_URL, env.SUPABASE_KEY, { auth: { persistSession: false } }); export function createAdapters(context: Context) { - const client = generateSupabase(context.config.supabase.url, context.config.supabase.key); return { supabase: { - access: new Access(client, context), - wallet: new Wallet(client, context), - user: new User(client, context), - debit: new Settlement(client, context), - settlement: new Settlement(client, context), - label: new Label(client, context), - logs: new Logs(client, context), - locations: new Locations(client, context), - super: new Super(client, context), + access: new Access(supabaseClient, context), + wallet: new Wallet(supabaseClient, context), + user: new User(supabaseClient, context), + debit: new Settlement(supabaseClient, context), + settlement: new Settlement(supabaseClient, context), + label: new Label(supabaseClient, context), + logs: new Logs(supabaseClient, context), + locations: new Locations(supabaseClient, context), + super: new Super(supabaseClient, context), }, }; } - -function generateSupabase(url?: string | null, key?: string | null) { - if (!url || !key) throw new Error("Supabase URL or key is not defined"); - return createClient(url, key, { auth: { persistSession: false } }); -} diff --git a/src/adapters/supabase/helpers/tables/logs.ts b/src/adapters/supabase/helpers/tables/logs.ts index 3203b6ff4..9a7b3dc7e 100644 --- a/src/adapters/supabase/helpers/tables/logs.ts +++ b/src/adapters/supabase/helpers/tables/logs.ts @@ -8,8 +8,9 @@ import { Database } from "../../types"; import { prettyLogs } from "../pretty-logs"; import { Super } from "./super"; import { execSync } from "child_process"; -import { Context } from "../../../../types"; +import { Context, LogLevel } from "../../../../types"; import Runtime from "../../../../bindings/bot-runtime"; +import { env } from "../../../../bindings/env"; type LogFunction = (message: string, metadata?: any) => void; type LogInsert = Database["public"]["Tables"]["logs"]["Insert"]; @@ -224,11 +225,10 @@ export class Logs extends Super { constructor(supabase: SupabaseClient, context: Context) { super(supabase, context); - const logConfig = this.context.config.log; - this.environment = logConfig.logEnvironment; - this.retryLimit = logConfig.retryLimit; - this.maxLevel = this._getNumericLevel(logConfig.level ?? LogLevel.DEBUG); + this.environment = env.LOG_ENVIRONMENT; + this.retryLimit = env.LOG_RETRY_LIMIT; + this.maxLevel = this._getNumericLevel(env.LOG_LEVEL); } private async _sendLogsToSupabase(log: LogInsert) { @@ -411,13 +411,3 @@ export class Logs extends Super { return obj; } } - -export enum LogLevel { - ERROR = "error", - WARN = "warn", - INFO = "info", - HTTP = "http", - VERBOSE = "verbose", - DEBUG = "debug", - SILLY = "silly", -} diff --git a/src/adapters/supabase/helpers/tables/wallet.test.ts b/src/adapters/supabase/helpers/tables/wallet.test.ts index 2464e9ed9..72a89c440 100644 --- a/src/adapters/supabase/helpers/tables/wallet.test.ts +++ b/src/adapters/supabase/helpers/tables/wallet.test.ts @@ -3,7 +3,7 @@ dotenv.config(); import { Context as ProbotContext } from "probot"; import { createAdapters } from "../../.."; -import { loadConfig } from "../../../../bindings/config"; +import { loadConfiguration } from "../../../../bindings/config"; import { Context, User } from "../../../../types"; const SUPABASE_URL = process.env.SUPABASE_URL; if (!SUPABASE_URL) throw new Error("SUPABASE_URL is not defined"); @@ -13,7 +13,7 @@ if (!SUPABASE_KEY) throw new Error("SUPABASE_KEY is not defined"); const mockContext = { supabase: { url: SUPABASE_URL, key: SUPABASE_KEY } } as unknown as ProbotContext; async function getWalletAddressAndUrlTest(eventContext: ProbotContext) { - const botConfig = await loadConfig(eventContext); + const botConfig = await loadConfiguration(eventContext); const context: Context = { event: eventContext, config: botConfig }; const { wallet } = createAdapters(context).supabase; const userId = 4975670 as User["id"]; diff --git a/src/bindings/config.ts b/src/bindings/config.ts index d77ef2d8e..de5302b63 100644 --- a/src/bindings/config.ts +++ b/src/bindings/config.ts @@ -1,24 +1,8 @@ -import ms from "ms"; -import { getPayoutConfigByNetworkId } from "../helpers"; -import { ajv, validateTypes } from "../utils"; -import { Context } from "probot"; +import { Context as ProbotContext } from "probot"; import { generateConfiguration } from "../utils/generate-configuration"; -import { LogLevel } from "../adapters/supabase/helpers/tables/logs"; -import Runtime from "./bot-runtime"; -import { - AllConfigurationTypes, - PublicConfigurationTypes, - PublicConfigurationValues, -} from "../types/configuration-types"; -import defaultConfiguration from "../ubiquibot-config-default"; +import { BotConfig } from "../types/configuration-types"; -const defaultConfigurationValidation = validateTypes(PublicConfigurationValues, defaultConfiguration); - -if (defaultConfigurationValidation.error) { - throw new Error(defaultConfigurationValidation.error); -} - -export async function loadConfiguration(context: Context): Promise { +export async function loadConfiguration(context: ProbotContext): Promise { // const runtime = Runtime.getState(); const configuration = await generateConfiguration(context); console.trace({ configuration }); diff --git a/src/bindings/env.ts b/src/bindings/env.ts new file mode 100644 index 000000000..cb153d147 --- /dev/null +++ b/src/bindings/env.ts @@ -0,0 +1,10 @@ +import { EnvConfig, validateEnvConfig } from "../types"; +import dotenv from "dotenv"; +dotenv.config(); + +export const env = { ...process.env } as unknown as EnvConfig; + +const valid = validateEnvConfig(env); +if (!valid) { + throw new Error("Invalid env configuration: " + JSON.stringify(validateEnvConfig.errors, null, 2)); +} diff --git a/src/bindings/event.ts b/src/bindings/event.ts index e77f18011..e6d068bcf 100644 --- a/src/bindings/event.ts +++ b/src/bindings/event.ts @@ -16,7 +16,7 @@ import { import { Payload } from "../types/payload"; import { ajv } from "../utils"; import Runtime from "./bot-runtime"; -import { loadConfig } from "./config"; +import { loadConfiguration } from "./config"; import { Context } from "../types"; const NO_VALIDATION = [GitHubEvent.INSTALLATION_ADDED_EVENT, GitHubEvent.PUSH_EVENT] as string[]; @@ -30,7 +30,7 @@ type AllHandlers = PreActionHandler | MainActionHandler | PostActionHandler; export async function bindEvents(eventContext: ProbotContext) { const runtime = Runtime.getState(); - const botConfig = await loadConfig(eventContext); + const botConfig = await loadConfiguration(eventContext); const context: Context = { event: eventContext, config: botConfig, @@ -39,7 +39,7 @@ export async function bindEvents(eventContext: ProbotContext) { runtime.adapters = createAdapters(context); runtime.logger = runtime.adapters.supabase.logs; - if (!context.config.payout.privateKey) { + if (!context.config.keys.evmPrivateEncrypted) { runtime.logger.warn("No EVM private key found"); } diff --git a/src/handlers/access/labels-access.ts b/src/handlers/access/labels-access.ts index db2123935..46e7dab35 100644 --- a/src/handlers/access/labels-access.ts +++ b/src/handlers/access/labels-access.ts @@ -5,7 +5,9 @@ import { Context, Payload, UserType } from "../../types"; export async function labelAccessPermissionsCheck(context: Context) { const runtime = Runtime.getState(); const logger = runtime.logger; - const { publicAccessControl } = context.config; + const { + features: { publicAccessControl }, + } = context.config; if (!publicAccessControl.setLabel) return true; const payload = context.event.payload as Payload; diff --git a/src/handlers/assign/action.ts b/src/handlers/assign/action.ts index 8365e5583..78a4a8819 100644 --- a/src/handlers/assign/action.ts +++ b/src/handlers/assign/action.ts @@ -33,7 +33,7 @@ export async function startCommandHandler(context: Context) { // Filter out labels that match the time labels defined in the config const timeLabelsAssigned: Label[] = labels.filter((label) => typeof label === "string" || typeof label === "object" - ? config.price.timeLabels.some((item) => item.name === label.name) + ? config.labels.time.some((item) => item.name === label.name) : false ); diff --git a/src/handlers/comment/action.ts b/src/handlers/comment/action.ts index 56c8974eb..d939ccec7 100644 --- a/src/handlers/comment/action.ts +++ b/src/handlers/comment/action.ts @@ -12,7 +12,7 @@ export async function commentCreatedOrEdited(context: Context) { const comment = payload.comment as Comment; const body = comment.body; - const commentedCommand = commentParser(context, body); + const commentedCommand = commentParser(body); if (!comment) { logger.info(`Comment is null. Skipping`); @@ -26,14 +26,14 @@ export async function commentCreatedOrEdited(context: Context) { await verifyFirstCommentInRepository(context); } - const allCommands = userCommands(context); + const allCommands = userCommands(config.miscellaneous.registerWalletWithVerification); const userCommand = allCommands.find((i) => i.id == commentedCommand); if (userCommand) { const { id, handler } = userCommand; logger.info("Running a comment handler", { id, handler: handler.name }); - const feature = config.command.find((e) => e.name === id.split("/")[1]); + const feature = config.commands.find((e) => e.name === id.split("/")[1]); if (feature?.enabled === false && id !== "/help") { return logger.warn("Skipping because it is disabled on this repo.", { id }); diff --git a/src/handlers/comment/handlers/assign/get-time-labels-assigned.ts b/src/handlers/comment/handlers/assign/get-time-labels-assigned.ts index 500387152..c6de00a40 100644 --- a/src/handlers/comment/handlers/assign/get-time-labels-assigned.ts +++ b/src/handlers/comment/handlers/assign/get-time-labels-assigned.ts @@ -9,7 +9,7 @@ export function getTimeLabelsAssigned(payload: Payload, config: BotConfig) { logger.warn("Skipping '/start' since no labels are set to calculate the timeline", { labels }); return; } - const timeLabelsDefined = config.price.timeLabels; + const timeLabelsDefined = config.labels.time; const timeLabelsAssigned: Label[] = []; for (const _label of labels) { const _labelType = typeof _label; diff --git a/src/handlers/comment/handlers/assign/index.ts b/src/handlers/comment/handlers/assign/index.ts index 03ef2cdcf..c690e21ab 100644 --- a/src/handlers/comment/handlers/assign/index.ts +++ b/src/handlers/comment/handlers/assign/index.ts @@ -19,9 +19,13 @@ export async function assign(context: Context, body: string) { const config = context.config; const payload = context.event.payload as Payload; const issue = payload.issue; + const { + miscellaneous: { maxConcurrentTasks }, + timers: { taskStaleTimeoutDuration }, + commands, + } = context.config; - const taskStaleTimeoutDuration = config.assign.taskStaleTimeoutDuration; - const startEnabled = config.command.find((command) => command.name === "start"); + const startEnabled = commands.find((command) => command.name === "start"); logger.info("Received '/start' command", { sender: payload.sender.login, body }); @@ -47,12 +51,12 @@ export async function assign(context: Context, body: string) { ); const assignedIssues = await getAssignedIssues(context, payload.sender.login); - logger.info("Max issue allowed is", config.assign.maxConcurrentTasks); + logger.info("Max issue allowed is", maxConcurrentTasks); // check for max and enforce max - if (assignedIssues.length - openedPullRequests.length >= config.assign.maxConcurrentTasks) { + if (assignedIssues.length - openedPullRequests.length >= maxConcurrentTasks) { throw logger.warn("Too many assigned issues, you have reached your max limit", { - maxConcurrentTasks: config.assign.maxConcurrentTasks, + maxConcurrentTasks, }); } diff --git a/src/handlers/comment/handlers/first.ts b/src/handlers/comment/handlers/first.ts index f266f4eec..2d35985dc 100644 --- a/src/handlers/comment/handlers/first.ts +++ b/src/handlers/comment/handlers/first.ts @@ -9,7 +9,9 @@ export async function verifyFirstCommentInRepository(context: Context) { throw runtime.logger.error("Issue is null. Skipping", { issue: payload.issue }, true); } const { - newContributorGreeting: { header, footer, enabled }, + features: { + newContributorGreeting: { header, footer, enabled }, + }, } = context.config; const response_issue = await context.event.octokit.rest.search.issuesAndPullRequests({ q: `is:issue repo:${payload.repository.owner.login}/${payload.repository.name} commenter:${payload.sender.login}`, diff --git a/src/handlers/comment/handlers/help.ts b/src/handlers/comment/handlers/help.ts index 57f1ed037..970db8f65 100644 --- a/src/handlers/comment/handlers/help.ts +++ b/src/handlers/comment/handlers/help.ts @@ -20,9 +20,9 @@ export async function listAvailableCommands(context: Context, body: string) { export function generateHelpMenu(context: Context) { const config = context.config; - const startEnabled = config.command.find((command) => command.name === "start"); + const startEnabled = config.commands.find((command) => command.name === "start"); let helpMenu = "### Available Commands\n\n| Command | Description | Example |\n| --- | --- | --- |\n"; - const commands = userCommands(context); + const commands = userCommands(config.miscellaneous.registerWalletWithVerification); commands.map( (command) => diff --git a/src/handlers/comment/handlers/index.ts b/src/handlers/comment/handlers/index.ts index 4dbedb445..2a2a299b3 100644 --- a/src/handlers/comment/handlers/index.ts +++ b/src/handlers/comment/handlers/index.ts @@ -1,4 +1,4 @@ -import { Context, UserCommands } from "../../../types"; +import { UserCommands } from "../../../types"; import { assign } from "./assign"; import { listAvailableCommands } from "./help"; // Commented out until Gnosis Safe is integrated (https://github.com/ubiquity/ubiquibot/issues/353) @@ -25,8 +25,8 @@ export * from "./unassign"; export * from "./wallet"; // Parses the comment body and figure out the command name a user wants -export function commentParser(context: Context, body: string): null | string { - const userCommandIds = userCommands(context).map((cmd) => cmd.id); +export function commentParser(body: string): null | string { + const userCommandIds = userCommands(false).map((cmd) => cmd.id); const regex = new RegExp(`^(${userCommandIds.join("|")})\\b`); // Regex pattern to match any command at the beginning of the body const matches = regex.exec(body); @@ -40,8 +40,8 @@ export function commentParser(context: Context, body: string): null | string { return null; } -export function userCommands(context: Context): UserCommands[] { - const accountForWalletVerification = walletVerificationDetails(context); +export function userCommands(walletVerificationEnabled: boolean): UserCommands[] { + const accountForWalletVerification = walletVerificationDetails(walletVerificationEnabled); return [ { id: "/start", @@ -112,7 +112,7 @@ export function userCommands(context: Context): UserCommands[] { ]; } -function walletVerificationDetails(context: Context) { +function walletVerificationDetails(walletVerificationEnabled: boolean) { const base = { description: "Register your wallet address for payments.", example: "/wallet ubq.eth", @@ -125,7 +125,6 @@ function walletVerificationDetails(context: Context) { "0xe2a3e34a63f3def2c29605de82225b79e1398190b542be917ef88a8e93ff9dc91bdc3ef9b12ed711550f6d2cbbb50671aa3f14a665b709ec391f3e603d0899a41b", }; - const walletVerificationEnabled = context.config.wallet.registerWalletWithVerification; if (walletVerificationEnabled) { return { description: `${base.description} ${withVerification.description}`, diff --git a/src/handlers/comment/handlers/issue/calculate-quality-score.ts b/src/handlers/comment/handlers/issue/calculate-quality-score.ts index 080086ffe..675518052 100644 --- a/src/handlers/comment/handlers/issue/calculate-quality-score.ts +++ b/src/handlers/comment/handlers/issue/calculate-quality-score.ts @@ -4,7 +4,7 @@ import { encodingForModel } from "js-tiktoken"; import Decimal from "decimal.js"; import Runtime from "../../../../bindings/bot-runtime"; -const openai = new OpenAI(); // apiKey: // defaults to process.env["OPENAI_API_KEY"] +//const openai = new OpenAI(); // apiKey: // defaults to process.env["OPENAI_API_KEY"] export async function calculateQualScore(issue: Issue, contributorComments: Comment[]) { const sumOfConversationTokens = countTokensOfConversation(issue, contributorComments); diff --git a/src/handlers/comment/handlers/issue/generate-permit-2-signature.ts b/src/handlers/comment/handlers/issue/generate-permit-2-signature.ts index 3ac70e638..d9e76e65a 100644 --- a/src/handlers/comment/handlers/issue/generate-permit-2-signature.ts +++ b/src/handlers/comment/handlers/issue/generate-permit-2-signature.ts @@ -4,6 +4,8 @@ import { BigNumber, ethers } from "ethers"; import { keccak256, toUtf8Bytes } from "ethers/lib/utils"; import Runtime from "../../../../bindings/bot-runtime"; import { Context } from "../../../../types"; +import { getPayoutConfigByNetworkId } from "../../../../helpers"; +import { decryptKeys } from "../../../../utils/private"; export async function generatePermit2Signature( context: Context, @@ -11,8 +13,11 @@ export async function generatePermit2Signature( ) { const runtime = Runtime.getState(); const { - payout: { privateKey, paymentToken, rpc, evmNetworkId }, + payments: { evmNetworkId }, + keys: { evmPrivateEncrypted }, } = context.config; + const { rpc, paymentToken } = getPayoutConfigByNetworkId(evmNetworkId); + const { privateKey } = await decryptKeys(evmPrivateEncrypted); if (!rpc) throw runtime.logger.error("RPC is not defined"); if (!privateKey) throw runtime.logger.error("Private key is not defined"); diff --git a/src/handlers/comment/handlers/issue/generate-permits.ts b/src/handlers/comment/handlers/issue/generate-permits.ts index 009ddb2c1..12131fd4d 100644 --- a/src/handlers/comment/handlers/issue/generate-permits.ts +++ b/src/handlers/comment/handlers/issue/generate-permits.ts @@ -7,6 +7,7 @@ import structuredMetadata from "../../../shared/structured-metadata"; import { generatePermit2Signature } from "./generate-permit-2-signature"; import { FinalScores } from "./issue-closed"; import { IssueRole } from "./_calculate-all-comment-scores"; +import { getPayoutConfigByNetworkId } from "../../../../helpers"; export async function generatePermits(context: Context, totals: FinalScores, contributorComments: Comment[]) { const userIdToNameMap = mapIdsToNames(contributorComments); @@ -23,8 +24,10 @@ async function generateComment( ) { const runtime = Runtime.getState(); const { - payout: { paymentToken, rpc, privateKey }, + payments: { evmNetworkId }, + keys: { evmPrivateEncrypted }, } = context.config; + const { rpc, paymentToken } = getPayoutConfigByNetworkId(evmNetworkId); const detailsTable = generateDetailsTable(totals, contributorComments); const tokenSymbol = await getTokenSymbol(paymentToken, rpc); const HTMLs = [] as string[]; @@ -38,7 +41,7 @@ async function generateComment( const contributorName = userIdToNameMap[userId]; const issueRole = userTotals.role; - if (!privateKey) throw runtime.logger.warn("No bot wallet private key defined"); + if (!evmPrivateEncrypted) throw runtime.logger.warn("No bot wallet private key defined"); const beneficiaryAddress = await runtime.adapters.supabase.wallet.getAddress(parseInt(userId)); diff --git a/src/handlers/comment/handlers/issue/issue-closed.ts b/src/handlers/comment/handlers/issue/issue-closed.ts index 6e4e2afed..97198a5d1 100644 --- a/src/handlers/comment/handlers/issue/issue-closed.ts +++ b/src/handlers/comment/handlers/issue/issue-closed.ts @@ -60,7 +60,7 @@ async function preflightChecks(context: Context, issue: Issue, logger: Logs, iss if (!issue) throw logger.error("Permit generation skipped because issue is undefined"); if (issue.state_reason !== StateReason.COMPLETED) throw logger.info("Issue was not closed as completed. Skipping.", { issue }); - if (config.publicAccessControl.fundExternalClosedIssue) { + if (config.features.publicAccessControl.fundExternalClosedIssue) { const userHasPermission = await checkUserPermissionForRepoAndOrg(context, payload.sender.login); if (!userHasPermission) throw logger.warn("Permit generation disabled because this issue has been closed by an external contributor."); diff --git a/src/handlers/comment/handlers/wallet.ts b/src/handlers/comment/handlers/wallet.ts index fbbb0de0b..a92c1e47e 100644 --- a/src/handlers/comment/handlers/wallet.ts +++ b/src/handlers/comment/handlers/wallet.ts @@ -42,7 +42,7 @@ export async function registerWallet(context: Context, body: string) { return logger.info("Skipping to register a wallet address because both address/ens doesn't exist"); } - if (config.wallet.registerWalletWithVerification) { + if (config.miscellaneous.registerWalletWithVerification) { _registerWalletWithVerification(body, address, logger); } diff --git a/src/handlers/pricing/pre.ts b/src/handlers/pricing/pre.ts index 1f61b82ab..fc6ef513e 100644 --- a/src/handlers/pricing/pre.ts +++ b/src/handlers/pricing/pre.ts @@ -11,15 +11,18 @@ export async function syncPriceLabelsToConfig(context: Context) { const config = context.config; const logger = runtime.logger; - const { assistivePricing } = config.features; + const { + features: { assistivePricing }, + labels, + } = config; if (!assistivePricing) { logger.info(`Assistive pricing is disabled`); return; } - const timeLabels = config.price.timeLabels.map((i) => i.name); - const priorityLabels = config.price.priorityLabels.map((i) => i.name); + const timeLabels = labels.time.map((i) => i.name); + const priorityLabels = labels.priority.map((i) => i.name); const aiLabels: string[] = []; for (const timeLabel of config.labels.time) { for (const priorityLabel of config.labels.priority) { diff --git a/src/handlers/pricing/pricing-label.ts b/src/handlers/pricing/pricing-label.ts index b25cd5fba..e641254ee 100644 --- a/src/handlers/pricing/pricing-label.ts +++ b/src/handlers/pricing/pricing-label.ts @@ -1,6 +1,6 @@ import Runtime from "../../bindings/bot-runtime"; import { addLabelToIssue, clearAllPriceLabelsOnIssue, createLabel, getAllLabeledEvents } from "../../helpers"; -import { BotConfig, Context, Label, LabelFromConfig, Payload, UserType } from "../../types"; +import { BotConfig, Context, Label, Payload, UserType } from "../../types"; import { labelAccessPermissionsCheck } from "../access"; import { setPrice } from "../shared/pricing"; import { handleParentIssue, isParentIssue, sortLabelsByValue } from "./action"; @@ -21,13 +21,13 @@ export async function onLabelChangeSetPricing(context: Context) { } const permission = await labelAccessPermissionsCheck(context); if (!permission) { - if (config.publicAccessControl.setLabel === false) { + if (config.features.publicAccessControl.setLabel === false) { throw logger.warn("No public access control to set labels"); } throw logger.warn("No permission to set labels"); } - const { assistivePricing } = config.mode; + const { assistivePricing } = config.features; if (!labels) throw logger.warn(`No labels to calculate price`); @@ -57,15 +57,13 @@ export async function onLabelChangeSetPricing(context: Context) { } function getRecognizedLabels(labels: Label[], config: BotConfig) { - const isRecognizedLabel = (label: Label, labelConfig: LabelFromConfig[]) => + const isRecognizedLabel = (label: Label, labelConfig: { name: string }[]) => (typeof label === "string" || typeof label === "object") && labelConfig.some((item) => item.name === label.name); - const recognizedTimeLabels: Label[] = labels.filter((label: Label) => - isRecognizedLabel(label, config.price.timeLabels) - ); + const recognizedTimeLabels: Label[] = labels.filter((label: Label) => isRecognizedLabel(label, config.labels.time)); const recognizedPriorityLabels: Label[] = labels.filter((label: Label) => - isRecognizedLabel(label, config.price.priorityLabels) + isRecognizedLabel(label, config.labels.priority) ); return { time: recognizedTimeLabels, priority: recognizedPriorityLabels }; diff --git a/src/handlers/push/index.ts b/src/handlers/push/index.ts index 3d79e0d08..a1262eb42 100644 --- a/src/handlers/push/index.ts +++ b/src/handlers/push/index.ts @@ -1,8 +1,8 @@ +import { Value } from "@sinclair/typebox/value"; import Runtime from "../../bindings/bot-runtime"; import { createCommitComment, getFileContent } from "../../helpers"; -import { CommitsPayload, PushPayload, ConfigSchema, Context } from "../../types"; -import { parseYamlConfig } from "../../utils/get-config"; -import { validate } from "../../utils/ajv"; +import { CommitsPayload, PushPayload, Context, validateBotConfig, BotConfigSchema } from "../../types"; +import { parseYaml } from "../../utils/generate-configuration"; export const ZERO_SHA = "0000000000000000000000000000000000000000"; export const BASE_RATE_FILE = ".github/ubiquibot-config.yml"; @@ -61,16 +61,17 @@ export async function validateConfigChange(context: Context) { if (configFileContent) { const decodedConfig = Buffer.from(configFileContent, "base64").toString(); - const config = parseYaml(decodedConfig); - const { valid, error } = validateTypes(PublicConfigurationValues, config); - if (!valid) { + let config = parseYaml(decodedConfig); + config = Value.Decode(BotConfigSchema, config); + const result = validateBotConfig(config); + if (!result) { await createCommitComment( context, - `@${payload.sender.login} Config validation failed! ${error}`, + `@${payload.sender.login} Config validation failed! ${validateBotConfig.errors}`, commitSha, BASE_RATE_FILE ); - logger.info("Config validation failed!", error); + logger.info("Config validation failed!", validateBotConfig.errors); } } } diff --git a/src/handlers/push/update-base-rate.ts b/src/handlers/push/update-base-rate.ts index ef2778df3..a4e402df6 100644 --- a/src/handlers/push/update-base-rate.ts +++ b/src/handlers/push/update-base-rate.ts @@ -2,7 +2,7 @@ import Runtime from "../../bindings/bot-runtime"; import { getPreviousFileContent, listLabelsForRepo, updateLabelsFromBaseRate } from "../../helpers"; import { Label, PushPayload, Context } from "../../types"; -import { parseYamlConfig } from "../../utils/get-config"; +import { parseYaml } from "../../utils/generate-configuration"; export async function updateBaseRate(context: Context, filePath: string) { const runtime = Runtime.getState(); diff --git a/src/handlers/shared/pricing.ts b/src/handlers/shared/pricing.ts index 80c7f32dc..325216a21 100644 --- a/src/handlers/shared/pricing.ts +++ b/src/handlers/shared/pricing.ts @@ -8,7 +8,7 @@ export function calculateTaskPrice( priorityValue: number, baseValue?: number ): number { - const base = baseValue ?? context.config.price.priceMultiplier; + const base = baseValue ?? context.config.payments.basePriceMultiplier; const priority = priorityValue / 10; // floats cause bad math const price = 1000 * base * timeValue * priority; return price; @@ -17,14 +17,14 @@ export function calculateTaskPrice( export function setPrice(context: Context, timeLabel: Label, priorityLabel: Label) { const runtime = Runtime.getState(); const logger = runtime.logger; - const { price } = context.config; + const { labels } = context.config; if (!timeLabel || !priorityLabel) throw logger.warn("Time or priority label is not defined"); - const recognizedTimeLabels = price.timeLabels.find((item) => item.name === timeLabel.name); + const recognizedTimeLabels = labels.time.find((item) => item.name === timeLabel.name); if (!recognizedTimeLabels) throw logger.warn("Time label is not recognized"); - const recognizedPriorityLabels = price.priorityLabels.find((item) => item.name === priorityLabel.name); + const recognizedPriorityLabels = labels.priority.find((item) => item.name === priorityLabel.name); if (!recognizedPriorityLabels) throw logger.warn("Priority label is not recognized"); const timeValue = calculateLabelValue(recognizedTimeLabels); diff --git a/src/handlers/wildcard/analytics.ts b/src/handlers/wildcard/analytics.ts index 5ab2254d7..003b242df 100644 --- a/src/handlers/wildcard/analytics.ts +++ b/src/handlers/wildcard/analytics.ts @@ -11,11 +11,10 @@ export function taskPaymentMetaData( priorityLabel: string | null; priceLabel: string | null; } { - const { price } = context.config; - const labels = issue.labels; + const { labels } = context.config; - const timeLabels = price.timeLabels.filter((item) => labels.map((i) => i.name).includes(item.name)); - const priorityLabels = price.priorityLabels.filter((item) => labels.map((i) => i.name).includes(item.name)); + const timeLabels = labels.time.filter((item) => issue.labels.map((i) => i.name).includes(item.name)); + const priorityLabels = labels.priority.filter((item) => issue.labels.map((i) => i.name).includes(item.name)); const isTask = timeLabels.length > 0 && priorityLabels.length > 0; @@ -29,7 +28,7 @@ export function taskPaymentMetaData( ? priorityLabels.reduce((a, b) => (calculateLabelValue(a) < calculateLabelValue(b) ? a : b)).name : null; - const priceLabel = labels.find((label) => label.name.includes("Price"))?.name || null; + const priceLabel = issue.labels.find((label) => label.name.includes("Price"))?.name || null; return { eligibleForPayment: isTask, diff --git a/src/handlers/wildcard/unassign/unassign.ts b/src/handlers/wildcard/unassign/unassign.ts index 340702987..753c3660c 100644 --- a/src/handlers/wildcard/unassign/unassign.ts +++ b/src/handlers/wildcard/unassign/unassign.ts @@ -23,8 +23,9 @@ async function checkTaskToUnassign(context: Context, assignedIssue: Issue) { const runtime = Runtime.getState(); const logger = runtime.logger; const payload = context.event.payload as Payload; - const unassign = context.config.unassign; - const { taskDisqualifyDuration, taskFollowUpDuration } = unassign; + const { + timers: { taskDisqualifyDuration, taskFollowUpDuration }, + } = context.config; logger.info("Checking for neglected tasks", { issueNumber: assignedIssue.number }); diff --git a/src/helpers/gpt.ts b/src/helpers/gpt.ts index bc1e3a6bb..ea06cf149 100644 --- a/src/helpers/gpt.ts +++ b/src/helpers/gpt.ts @@ -136,21 +136,22 @@ export async function askGPT(context: Context, chatHistory: CreateChatCompletion const runtime = Runtime.getState(); const logger = runtime.logger; const config = context.config; + const { keys } = config; - if (!config.ask.apiKey) { + if (!keys.openAi) { throw logger.error( "You must configure the `openai-api-key` property in the bot configuration in order to use AI powered features." ); } const openAI = new OpenAI({ - apiKey: config.ask.apiKey, + apiKey: keys.openAi, }); const res: OpenAI.Chat.Completions.ChatCompletion = await openAI.chat.completions.create({ messages: chatHistory, model: "gpt-3.5-turbo-16k", - max_tokens: config.ask.tokenLimit, + max_tokens: config.openai.tokenLimit, temperature: 0, }); diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts index bde98f35b..acdd5c3c9 100644 --- a/src/helpers/issue.ts +++ b/src/helpers/issue.ts @@ -645,8 +645,7 @@ export async function getCommitsOnPullRequest(context: Context, pullNumber: numb } export async function getAvailableOpenedPullRequests(context: Context, username: string) { - const unassignConfig = context.config.unassign; - const { reviewDelayTolerance } = unassignConfig; + const { reviewDelayTolerance } = context.config.timers; if (!reviewDelayTolerance) return []; const openedPullRequests = await getOpenedPullRequests(context, username); diff --git a/src/helpers/label.ts b/src/helpers/label.ts index 3ee47f9db..a43650c2a 100644 --- a/src/helpers/label.ts +++ b/src/helpers/label.ts @@ -56,13 +56,13 @@ export async function updateLabelsFromBaseRate( const newLabels: string[] = []; const previousLabels: string[] = []; - for (const timeLabel of config.price.timeLabels) { - for (const priorityLabel of config.price.priorityLabels) { + for (const timeLabel of config.labels.time) { + for (const priorityLabel of config.labels.priority) { const targetPrice = calculateTaskPrice( context, calculateLabelValue(timeLabel), calculateLabelValue(priorityLabel), - config.price.basePriceMultiplier + config.payments.basePriceMultiplier ); const targetPriceLabel = `Price: ${targetPrice} USD`; newLabels.push(targetPriceLabel); diff --git a/src/helpers/shared.ts b/src/helpers/shared.ts index d2b37cad7..72c00827c 100644 --- a/src/helpers/shared.ts +++ b/src/helpers/shared.ts @@ -1,5 +1,5 @@ import ms from "ms"; -import { Label, LabelFromConfig, Payload, UserType, Context } from "../types"; +import { Label, Payload, UserType, Context } from "../types"; const contextNamesToSkip = ["workflow_run"]; @@ -18,7 +18,7 @@ export function shouldSkip(context: Context) { return response; } -export function calculateLabelValue(label: LabelFromConfig): number { +export function calculateLabelValue(label: { name: string }): number { const matches = label.name.match(/\d+/); const number = matches && matches.length > 0 ? parseInt(matches[0]) || 0 : 0; if (label.name.toLowerCase().includes("priority")) return number; diff --git a/src/types/configuration-types.ts b/src/types/configuration-types.ts index 4586c2cdd..4ae95900c 100644 --- a/src/types/configuration-types.ts +++ b/src/types/configuration-types.ts @@ -1,7 +1,9 @@ -import { Type as T, Static } from "@sinclair/typebox"; -import { LogLevel } from "../adapters/supabase/helpers/tables/logs"; +import { Type as T, Static, TProperties, TObject, ObjectOptions, StringOptions, StaticDecode } from "@sinclair/typebox"; +import { LogLevel } from "../types"; import { validHTMLElements } from "../handlers/comment/handlers/issue/valid-html-elements"; -import z from "zod"; +import { userCommands } from "../handlers"; +import { ajv } from "../utils"; +import ms from "ms"; const promotionComment = "###### If you enjoy the DevPool experience, please follow [Ubiquity on GitHub](https://github.com/ubiquity) and star [this repo](https://github.com/ubiquity/devpool-directory) to show your support. It helps a lot!"; @@ -10,13 +12,12 @@ const defaultGreetingHeader = const HtmlEntities = validHTMLElements.map((value) => T.Literal(value)); -const allHtmlElementsSetToZero = validHTMLElements.reduce>( - (accumulator, current) => { - accumulator[current] = 0; - return accumulator; - }, - {} as Record -); +const allHtmlElementsSetToZero = validHTMLElements.reduce((accumulator, current) => { + accumulator[current] = 0; + return accumulator; +}, {} as Record); + +const allCommands = userCommands(false).map((cmd) => ({ name: cmd.id.replace("/", ""), enabled: false })); const defaultTimeLabels = [ { name: "Time: <1 Hour" }, @@ -134,12 +135,23 @@ export const BotConfigSchema = z.strictObject({ export type BotConfig = z.infer; */ -const StrictObject = (obj: Parameters[0], options?: Parameters[1]) => - T.Object(obj, { additionalProperties: false, default: {}, ...options }); +function StrictObject(obj: T, options?: ObjectOptions): TObject { + return T.Object(obj, { additionalProperties: false, default: {}, ...options }); +} + +function stringDuration(options?: StringOptions) { + return T.Transform(T.String(options)) + .Decode((value) => { + return ms(value); + }) + .Encode((value) => { + return ms(value); + }); +} -export const EnvConfigSchema = StrictObject({ +export const EnvConfigSchema = T.Object({ WEBHOOK_PROXY_URL: T.String({ format: "uri" }), - LOG_ENVIRONMENT: T.String({ default: "development" }), + LOG_ENVIRONMENT: T.String({ default: "production" }), LOG_LEVEL: T.Enum(LogLevel), LOG_RETRY_LIMIT: T.Number({ default: 8 }), SUPABASE_URL: T.String({ format: "uri" }), @@ -149,17 +161,19 @@ export const EnvConfigSchema = StrictObject({ APP_ID: T.Number(), }); +export const validateEnvConfig = ajv.compile(EnvConfigSchema); export type EnvConfig = Static; export const BotConfigSchema = StrictObject({ keys: StrictObject({ evmPrivateEncrypted: T.String(), - openAi: T.String(), + openAi: T.Optional(T.String()), }), features: StrictObject({ assistivePricing: T.Boolean({ default: false }), defaultLabels: T.Array(T.String(), { default: [] }), newContributorGreeting: StrictObject({ + enabled: T.Boolean({ default: false }), header: T.String({ default: defaultGreetingHeader }), displayHelpMenu: T.Boolean({ default: true }), footer: T.String({ default: promotionComment }), @@ -169,11 +183,14 @@ export const BotConfigSchema = StrictObject({ fundExternalClosedIssue: T.Boolean({ default: true }), }), }), + openai: StrictObject({ + tokenLimit: T.Number({ default: 100000 }), + }), timers: StrictObject({ - reviewDelayTolerance: T.String({ default: "1 day" }), - taskStaleTimeoutDuration: T.String({ default: "1 month" }), - taskFollowUpDuration: T.String({ default: "0.5 weeks" }), - taskDisqualifyDuration: T.String({ default: "1 week" }), + reviewDelayTolerance: stringDuration({ default: "1 day" }), + taskStaleTimeoutDuration: stringDuration({ default: "1 month" }), + taskFollowUpDuration: stringDuration({ default: "0.5 weeks" }), + taskDisqualifyDuration: stringDuration({ default: "1 week" }), }), payments: StrictObject({ maxPermitPrice: T.Number({ default: Number.MAX_SAFE_INTEGER }), @@ -184,8 +201,9 @@ export const BotConfigSchema = StrictObject({ commands: T.Array( StrictObject({ name: T.String(), - enabled: T.Boolean({ default: false }), - }) + enabled: T.Boolean(), + }), + { default: allCommands } ), incentives: StrictObject({ comment: StrictObject({ @@ -209,5 +227,6 @@ export const BotConfigSchema = StrictObject({ registerWalletWithVerification: T.Boolean({ default: false }), }), }); +export const validateBotConfig = ajv.compile(BotConfigSchema); -export type BotConfig = Static; +export type BotConfig = StaticDecode; diff --git a/src/types/index.ts b/src/types/index.ts index 3386eee3b..9fd5354e3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,3 +4,5 @@ export * from "./handlers"; export * from "./configuration-types"; export * from "./markdown"; export * from "./context"; +export * from "./openai"; +export * from "./logs"; diff --git a/src/types/logs.ts b/src/types/logs.ts new file mode 100644 index 000000000..8e3e1b913 --- /dev/null +++ b/src/types/logs.ts @@ -0,0 +1,9 @@ +export enum LogLevel { + ERROR = "error", + WARN = "warn", + INFO = "info", + HTTP = "http", + VERBOSE = "verbose", + DEBUG = "debug", + SILLY = "silly", +} diff --git a/src/types/openai.ts b/src/types/openai.ts new file mode 100644 index 000000000..e4b62d6bb --- /dev/null +++ b/src/types/openai.ts @@ -0,0 +1,19 @@ +import { Type as T, Static } from "@sinclair/typebox"; + +export const StreamlinedCommentSchema = T.Object({ + login: T.Optional(T.String()), + body: T.Optional(T.String()), +}); + +export type StreamlinedComment = Static; + +export const GPTResponseSchema = T.Object({ + answer: T.Optional(T.String()), + tokenUsage: T.Object({ + output: T.Optional(T.Number()), + input: T.Optional(T.Number()), + total: T.Optional(T.Number()), + }), +}); + +export type GPTResponse = Static; diff --git a/src/ubiquibot-config-default.ts b/src/ubiquibot-config-default.ts index bc59ca416..95952fa31 100644 --- a/src/ubiquibot-config-default.ts +++ b/src/ubiquibot-config-default.ts @@ -1,7 +1,7 @@ import fs from "fs"; import path from "path"; import { validHTMLElements } from "./handlers/comment/handlers/issue/valid-html-elements"; -import { LogLevel } from "./adapters/supabase/helpers/tables/logs"; +import { LogLevel } from "./types"; const commandFiles = fs.readdirSync(path.resolve(__dirname, "../src/handlers/comment/handlers")); const commands = commandFiles.map((file) => { diff --git a/src/utils/generate-configuration.ts b/src/utils/generate-configuration.ts index f31902d03..d997a0c3d 100644 --- a/src/utils/generate-configuration.ts +++ b/src/utils/generate-configuration.ts @@ -1,22 +1,18 @@ -import sodium from "libsodium-wrappers"; import merge from "lodash/merge"; import { Context } from "probot"; import YAML from "yaml"; import Runtime from "../bindings/bot-runtime"; -import { upsertLastCommentToIssue } from "../helpers/issue"; -import { Payload, BotConfigSchema, BotConfig } from "../types"; -import defaultConfiguration from "../ubiquibot-config-default"; -import { validateTypes } from "./ajv"; -import { z } from "zod"; +import { Payload, BotConfig, validateBotConfig, BotConfigSchema } from "../types"; +import { DefinedError } from "ajv"; +import { Value } from "@sinclair/typebox/value"; const UBIQUIBOT_CONFIG_REPOSITORY = "ubiquibot-config"; const UBIQUIBOT_CONFIG_FULL_PATH = ".github/ubiquibot-config.yml"; -const KEY_PREFIX = "HSK_"; export async function generateConfiguration(context: Context): Promise { const payload = context.payload as Payload; - const organizationConfiguration = parseYaml( + let organizationConfiguration = parseYaml( await download({ context, repository: UBIQUIBOT_CONFIG_REPOSITORY, @@ -24,7 +20,7 @@ export async function generateConfiguration(context: Context): Promise issue.code !== z.ZodIssueCode.unrecognized_keys + organizationConfiguration = Value.Decode(BotConfigSchema, organizationConfiguration); + const valid = validateBotConfig(organizationConfiguration); + if (!valid) { + const errors = (validateBotConfig.errors as DefinedError[]).filter( + (error) => !(error.keyword === "required" && error.params.missingProperty === "evmPrivateEncrypted") ); - if (errorsWithoutStrict.length > 0) { - const err = new Error(result.error.toString()); - throw err; - } else { - // make comment - } + const err = generateValidationError(errors as DefinedError[]); + if (err instanceof Error) throw err; + if (payload.issue?.number) + await context.octokit.issues.createComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: payload.issue?.number, + body: err, + }); } - orgConfig = result.data; + orgConfig = organizationConfiguration as BotConfig; } let repoConfig: BotConfig | undefined; if (repositoryConfiguration) { console.dir(repositoryConfiguration, { depth: null, colors: true }); - const result = BotConfigSchema.safeParse(repositoryConfiguration); - if (!result.success) { - // TODO - } else { - repoConfig = result.data; + repositoryConfiguration = Value.Decode(BotConfigSchema, repositoryConfiguration); + const valid = validateBotConfig(repositoryConfiguration); + if (!valid) { + const errors = (validateBotConfig.errors as DefinedError[]).filter( + (error) => !(error.keyword === "required" && error.params.missingProperty === "evmPrivateEncrypted") + ); + const err = generateValidationError(errors as DefinedError[]); + if (err instanceof Error) throw err; + if (payload.issue?.number) + await context.octokit.issues.createComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: payload.issue?.number, + body: err, + }); } + repoConfig = repositoryConfiguration as BotConfig; } const merged = merge({}, orgConfig, repoConfig); - const result = BotConfigSchema.safeParse(merged); - if (!result.success) { - // TODO - /*const issue = payload.issue?.number; - const err = new Error(error?.toString()); - if (issue) await upsertLastCommentToIssue(issue, err.message); - throw err;*/ - } else { - return result.data; + const valid = validateBotConfig(merged); + if (!valid) { + const err = generateValidationError(validateBotConfig.errors as DefinedError[]); + if (err instanceof Error) throw err; + if (payload.issue?.number) + await context.octokit.issues.createComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: payload.issue?.number, + body: err, + }); } + return merged as BotConfig; +} + +function generateValidationError(errors: DefinedError[]): Error | string { + const errorsWithoutStrict = errors.filter((error) => error.keyword !== "additionalProperties"); + const errorsOnlyStrict = errors.filter((error) => error.keyword === "additionalProperties"); + const isValid = errorsWithoutStrict.length === 0; + const message = `${isValid ? "Valid" : "Invalid"} configuration. +${!isValid && "Errors: \n" + errorsWithoutStrict.map((error) => error.message).join("\n")} +${ + errorsOnlyStrict.length > 0 + ? `Warning! Unneccesary properties: + ${errorsOnlyStrict + .map( + (error) => + error.keyword === "additionalProperties" && error.instancePath + "/" + error.params.additionalProperty + ) + .join("\n")}` + : "" +}`; + return isValid ? message : new Error(message); } // async function fetchConfigurations(context: Context, type: "org" | "repo") { @@ -130,36 +164,3 @@ export function parseYaml(data: null | string) { } return null; } - -async function decryptKeys(cipherText: string) { - await sodium.ready; - - let _public: null | string = null; - let _private: null | string = null; - - const X25519_PRIVATE_KEY = process.env.X25519_PRIVATE_KEY; - - if (!X25519_PRIVATE_KEY) { - console.warn("X25519_PRIVATE_KEY is not defined"); - return { private: null, public: null }; - } - _public = await getScalarKey(X25519_PRIVATE_KEY); - if (!_public) { - console.warn("Public key is null"); - return { private: null, public: null }; - } - const binPub = sodium.from_base64(_public, sodium.base64_variants.URLSAFE_NO_PADDING); - const binPriv = sodium.from_base64(X25519_PRIVATE_KEY, sodium.base64_variants.URLSAFE_NO_PADDING); - const binCipher = sodium.from_base64(cipherText, sodium.base64_variants.URLSAFE_NO_PADDING); - - const walletPrivateKey: string | null = sodium.crypto_box_seal_open(binCipher, binPub, binPriv, "text"); - _private = walletPrivateKey?.replace(KEY_PREFIX, ""); - return { private: _private, public: _public }; -} - -async function getScalarKey(X25519_PRIVATE_KEY: string) { - await sodium.ready; - const binPriv = sodium.from_base64(X25519_PRIVATE_KEY, sodium.base64_variants.URLSAFE_NO_PADDING); - const scalerPub = sodium.crypto_scalarmult_base(binPriv, "base64"); - return scalerPub; -} diff --git a/src/utils/private.ts b/src/utils/private.ts new file mode 100644 index 000000000..040e90f4b --- /dev/null +++ b/src/utils/private.ts @@ -0,0 +1,38 @@ +import sodium from "libsodium-wrappers"; +import { env } from "../bindings/env"; +const KEY_PREFIX = "HSK_"; + +export async function decryptKeys( + cipherText: string +): Promise<{ privateKey: string; publicKey: string } | { privateKey: null; publicKey: null }> { + await sodium.ready; + + let _public: null | string = null; + let _private: null | string = null; + + const { X25519_PRIVATE_KEY } = env; + + if (!X25519_PRIVATE_KEY) { + console.warn("X25519_PRIVATE_KEY is not defined"); + return { privateKey: null, publicKey: null }; + } + _public = await getScalarKey(X25519_PRIVATE_KEY); + if (!_public) { + console.warn("Public key is null"); + return { privateKey: null, publicKey: null }; + } + const binPub = sodium.from_base64(_public, sodium.base64_variants.URLSAFE_NO_PADDING); + const binPriv = sodium.from_base64(X25519_PRIVATE_KEY, sodium.base64_variants.URLSAFE_NO_PADDING); + const binCipher = sodium.from_base64(cipherText, sodium.base64_variants.URLSAFE_NO_PADDING); + + const walletPrivateKey: string | null = sodium.crypto_box_seal_open(binCipher, binPub, binPriv, "text"); + _private = walletPrivateKey?.replace(KEY_PREFIX, ""); + return { privateKey: _private, publicKey: _public }; +} + +async function getScalarKey(X25519_PRIVATE_KEY: string) { + await sodium.ready; + const binPriv = sodium.from_base64(X25519_PRIVATE_KEY, sodium.base64_variants.URLSAFE_NO_PADDING); + const scalerPub = sodium.crypto_scalarmult_base(binPriv, "base64"); + return scalerPub; +} diff --git a/yarn.lock b/yarn.lock index 040095a5b..a437cf3dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2399,10 +2399,10 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== -"@sinclair/typebox@^0.31.5": - version "0.31.20" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.31.20.tgz#74e855ba87a795f10c1eb9d791c7316d930d292e" - integrity sha512-pqkf2X6fc1yk6c3Rk41cT8NYFKmzngl8TVHy375X7ihlONCsX7/YTReRLyZX7zZhuUUBx9KTZPFFDZo6AIERCw== +"@sinclair/typebox@^0.31.22": + version "0.31.22" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.31.22.tgz#f13fa4050a7e883d252365902e38186fa0dc8ab8" + integrity sha512-CKviMgpcXd8q8IsQQD8cCleswe4/EkQRcOqtVQcP1e+XUyszjJYjgL5Dtf3XunWZc2zEGmQPqJEsq08NiW9xfw== "@sinonjs/commons@^3.0.0": version "3.0.0"