Initial Version (Working relay implementing basic functionality)

This commit is contained in:
Danny Morabito 2025-02-07 13:22:49 +01:00
commit aeae39df4d
Signed by: dannym
GPG key ID: 7CC8056A5A04557E
15 changed files with 1272 additions and 0 deletions

33
utils/encryption.ts Normal file
View file

@ -0,0 +1,33 @@
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
import { managedNonce } from "@noble/ciphers/webcrypto";
import { decodeBase64 } from "jsr:@std/encoding/base64";
export const encryptionKey = decodeBase64(Deno.env.get("ENCRYPTION_KEY") || "");
/**
* Encrypts a given Uint8Array using the XChaCha20-Poly1305 algorithm.
*
* @param data - The data to be encrypted as a Uint8Array.
* @param key - The encryption key as a Uint8Array.
* @returns The encrypted data as a Uint8Array.
*/
export function encryptUint8Array(
data: Uint8Array,
key: Uint8Array,
): Uint8Array {
return managedNonce(xchacha20poly1305)(key).encrypt(data);
}
/**
* Decrypts a given Uint8Array using the XChaCha20-Poly1305 algorithm.
*
* @param data - The data to be decrypted as a Uint8Array.
* @param key - The decryption key as a Uint8Array.
* @returns The decrypted data as a Uint8Array.
*/
export function decryptUint8Array(
data: Uint8Array,
key: Uint8Array,
): Uint8Array {
return managedNonce(xchacha20poly1305)(key).decrypt(data);
}

31
utils/files.ts Normal file
View file

@ -0,0 +1,31 @@
import { exists } from "jsr:@std/fs";
/**
* Return the path to Eve's configuration directory.
*
* The configuration directory is resolved in the following order:
* 1. The value of the `XDG_CONFIG_HOME` environment variable.
* 2. The value of the `HOME` environment variable, with `.config` appended.
*
* If the resolved path does not exist, create it.
*/
export async function getEveConfigHome(): Promise<string> {
const xdgConfigHome = Deno.env.get("XDG_CONFIG_HOME") ??
`${Deno.env.get("HOME")}/.config`;
const storagePath = `${xdgConfigHome}/arx/Eve`;
if (!(await exists(storagePath))) {
await Deno.mkdir(storagePath, { recursive: true });
}
return storagePath;
}
/**
* Return the path to the file in Eve's configuration directory.
*
* @param file The name of the file to return the path for.
* @returns The path to the file in Eve's configuration directory.
*/
export async function getEveFilePath(file: string): Promise<string> {
const storagePath = await getEveConfigHome();
return `${storagePath}/${file}`;
}

75
utils/logs.ts Normal file
View file

@ -0,0 +1,75 @@
import * as colors from "jsr:@std/fmt@^1.0.4/colors";
import * as log from "jsr:@std/log";
import { getEveFilePath } from "./files.ts";
export * as log from "jsr:@std/log";
export async function setupLogger() {
const formatLevel = (level: number): string => {
return (
{
10: colors.gray("[DEBUG]"),
20: colors.green("[INFO] "),
30: colors.yellow("[WARN] "),
40: colors.red("[ERROR]"),
50: colors.bgRed("[FATAL]"),
}[level] || `[LVL${level}]`
);
};
const levelName = (level: number): string => {
return {
10: "DEBUG",
20: "INFO",
30: "WARN",
40: "ERROR",
50: "FATAL",
}[level] || `LVL${level}`;
};
const formatArg = (arg: unknown): string => {
if (typeof arg === "object") return JSON.stringify(arg);
return String(arg);
};
await log.setup({
handlers: {
console: new log.ConsoleHandler("DEBUG", {
useColors: true,
formatter: (record) => {
const timestamp = new Date().toISOString();
let msg = `${colors.dim(`[${timestamp}]`)} ${
formatLevel(record.level)
} ${record.msg}`;
if (record.args.length > 0) {
const args = record.args
.map((arg, i) => `${colors.dim(`arg${i}:`)} ${formatArg(arg)}`)
.join(" ");
msg += ` ${colors.dim("|")} ${args}`;
}
return msg;
},
}),
file: new log.FileHandler("DEBUG", {
filename: Deno.env.get("LOG_FILE") ||
await getEveFilePath("eve-logs.jsonl"),
formatter: (record) => {
const timestamp = new Date().toISOString();
return JSON.stringify({
timestamp,
level: levelName(record.level),
msg: record.msg,
args: record.args,
});
},
}),
},
loggers: {
default: {
level: "DEBUG",
handlers: ["console", "file"],
},
},
});
}

99
utils/queries.ts Normal file
View file

@ -0,0 +1,99 @@
import type { BindValue, Database } from "@db/sqlite";
/**
* Construct a SQL query with placeholders for values.
*
* This function takes a template string and interpolates it with the given
* values, replacing placeholders with `?`.
*
* @example
* const query = sqlPartial`SELECT * FROM events WHERE id = ? OR id = ?`,
* ['1', '2'];
* // query = {
* // query: 'SELECT * FROM events WHERE id = ? OR id = ?',
* // values: ['1', '2']
* // }
*
* @param {TemplateStringsArray} segments A template string
* @param {...BindValue[]} values Values to interpolate
* @returns {{ query: string, values: BindValue[] }} A SQL query with placeholders
*/
export function sqlPartial(
segments: TemplateStringsArray,
...values: BindValue[]
) {
return {
query: segments.reduce(
(acc, str, i) => acc + str + (i < values.length ? "?" : ""),
"",
),
values: values,
};
}
/**
* Construct a SQL query with placeholders for values and return a function
* that executes that query on a database.
*
* This is a convenience wrapper around `sqlPartial` and `sqlPartialRunner`.
*
* @example
* const run = sql`SELECT * FROM events WHERE id = ? OR id = ?`,
* ['1', '2'];
* const results = run(db);
*
* @param {TemplateStringsArray} segments A template string
* @param {...BindValue[]} values Values to interpolate
* @returns {Function} A function that takes a Database and returns the query results
*/
export function sql(segments: TemplateStringsArray, ...values: BindValue[]) {
return sqlPartialRunner(sqlPartial(segments, ...values));
}
/**
* Combine multiple partial queries into a single query.
*
* This function takes any number of partial queries with values and combines
* them into a single query.
*
* @example
* const query1 = { query: 'SELECT * FROM foo', values: [] };
* const query2 = { query: 'WHERE bar = ?', values: ['5'] };
* const query = mixQuery(query1, query2);
* // query = {
* // query: 'SELECT * FROM foo WHERE bar = ?',
* // values: ['5']
* // }
*
* @param {...{ query: string, values: BindValue[] }} queries Partial queries
* @returns {{ query: string, values: BindValue[] }} A combined query
*/
export function mixQuery(...queries: { query: string; values: BindValue[] }[]) {
const { query, values } = queries.reduce(
(acc, { query, values }) => ({
query: `${acc.query} ${query}`,
values: [...acc.values, ...values],
}),
{ query: "", values: [] },
);
return { query, values };
}
/**
* Executes a SQL query against a database.
*
* This function takes a query object containing a SQL query string with placeholders
* and an array of values. It returns a function that, when given a Database instance,
* prepares the query and executes it, returning all results.
*
* @param {Object} query An object containing the SQL query string and corresponding values
* @returns {Function} A function that takes a Database instance and returns the query results
*/
export function sqlPartialRunner(query: {
query: string;
values: BindValue[];
}) {
const run = (db: Database) => db.prepare(query.query).all(...query.values);
return run;
}