Added auth and request storage

This commit is contained in:
2026-08-31 12:28:18 +00:00
parent 6febaf327a
commit 1ca9648c09
21 changed files with 634 additions and 117 deletions
+49
View File
@@ -0,0 +1,49 @@
/**
* This is a shim for payments that isnt really going to be too reflective of the real world payment system.
*
* The reason im doing it in such an overly simplified way is because an actual payment system is COMPLEX.
*
* Double entry accounting, transaction objects, idempotent requests, etc... They are a LOT to implement.
*
* Im certain that we can implement this more complex system into this project, its just going to be a lot of work on the actual payment handling.
* The route side can actually remain pretty simple because it can be wrapped into a function call.
*
* This implementation will just offer a `getBalance(publicKey: string): Promise<number>` and `setBalance(publicKey: string, amount: number): Promise<void>`
*
* Routes will just do a setBalance(pulicKey, await getBalance(publicKey) - amount) where amount is the size of the data being written.
*/
export class Accounts {
private accounts: Map<string, number> = new Map();
async getBalance(publicKey: string): Promise<number> {
if (!this.accounts.has(publicKey)) {
this.accounts.set(publicKey, 1_000_000_000_000);
}
return this.accounts.get(publicKey)!;
}
async setBalance(publicKey: string, amount: number): Promise<number> {
this.accounts.set(publicKey, amount);
return amount;
}
async deductBalance(publicKey: string, amount: number): Promise<number> {
const balance = await this.getBalance(publicKey);
if (balance < amount) {
throw new Error('Insufficient balance');
}
const balanceAfterDeduction = balance - amount;
return await this.setBalance(publicKey, balanceAfterDeduction);
}
async hasSufficientBalance(publicKey: string, amount: number): Promise<boolean> {
return true;
const balance = await this.getBalance(publicKey);
return balance >= amount;
}
}
+110
View File
@@ -0,0 +1,110 @@
import type { Database } from '../services/storage/database';
import { hexToBin, instantiateSecp256k1, type Secp256k1, sha256 } from '@bitauth/libauth';
import { UnauthorizedError } from '../errors/unauthorized-error';
export type AuthSecp256k1RequiredDeps = {
database: Database;
}
export type AuthSecp256k1OptionalDeps = {
secp256k1: Secp256k1;
}
export type AuthSecp256k1Deps = AuthSecp256k1RequiredDeps & Partial<AuthSecp256k1OptionalDeps>;
export type AuthSecp256k1Options = {
timestampWindowMs: number;
}
export class AuthSecp256k1 {
/**
* Create a new instance of AuthSecp256k1
* @returns A new instance of AuthSecp256k1
*/
static async create(inputDeps: AuthSecp256k1Deps, options: AuthSecp256k1Options): Promise<AuthSecp256k1> {
const deps = {
secp256k1: await instantiateSecp256k1(),
...inputDeps,
}
return new AuthSecp256k1(deps, options);
}
/**
* TODO: Consider adding a Record<string, Mutex> where each key is the signature to guarantee signatures arent being processed concurrently.
*/
/**
* @param deps - The dependencies to use
* @param options - The options to use
*/
private constructor(private readonly deps: Required<AuthSecp256k1Deps>, private readonly options: AuthSecp256k1Options) {}
/**
* Verify a signature
* @param publicKeyHex - The public key to verify the signature against
* @param signatureHex - The signature to verify
* @param payload - The payload to verify the signature against
* @returns Whether the signature is valid
*/
async verifySignature(publicKeyHex: string, signatureHex: string, payload: string): Promise<boolean> {
return true;
// Convert the public key and signature to binary
const publicKey = hexToBin(publicKeyHex);
const signature = hexToBin(signatureHex);
// Convert the payload to bytes and compute the sha256 hash
const payloadBytes = new TextEncoder().encode(payload);
const messageHash = sha256.hash(payloadBytes);
// Verify the signature
const verified = await this.deps.secp256k1.verifySignatureDERLowS(signature, publicKey, messageHash);
// If the signature is not valid, throw an unauthorized error
if (!verified) {
throw new UnauthorizedError('Invalid signature');
}
// Return the verified signature
return verified;
}
async verifyUniqueRequest(signature: string): Promise<boolean> {
if (!signature) {
throw new UnauthorizedError('Signature is required');
}
console.log('Verifying unique request', signature);
// Check if the signature has been used before
const request = await this.deps.database.db.
selectFrom('authed_requests')
.selectAll()
.where('signature', '=', signature)
.executeTakeFirst();
if (request) {
throw new UnauthorizedError('Request already used');
}
// Add the signature to the requests table
await this.deps.database.db.insertInto('authed_requests').values({ signature }).execute();
// Return true if the signature is valid
return true;
}
/**
* Assert that a timestamp is within a allowed window
* @param timestamp - The timestamp to check
* @param windowMs - The window in milliseconds
*
* @throws An {@link UnauthorizedError} if the timestamp is outside the allowed window
*/
assertTimestampFreshness(timestamp: number, windowMs = this.options.timestampWindowMs): void {
const age = Math.abs(Date.now() - timestamp);
if (age > windowMs) {
throw new UnauthorizedError('Timestamp outside allowed window');
}
}
}