Template
1
0

feat: modular domain driven boilerplate

This commit is contained in:
2025-09-22 01:29:55 +02:00
parent 2433f59d1a
commit 9be3230c84
160 changed files with 2468 additions and 1525 deletions

51
platform/vault/hmac.ts Normal file
View File

@@ -0,0 +1,51 @@
/**
* Hash a value with given secret.
*
* @param value - Value to hash.
* @param secret - Secret to hash the value against.
*/
export async function hash(value: string, secret: string): Promise<string> {
const key = await getImportKey(secret, ["sign"]);
const encoder = new TextEncoder();
const valueData = encoder.encode(value);
const signature = await crypto.subtle.sign("HMAC", key, valueData);
return bufferToHex(signature);
}
/**
* Verify that the given value results in the expected hash using the provided secret.
*
* @param value - Value to verify.
* @param expectedHash - Expected hash value.
* @param secret - Secret used to hash the value.
*/
export async function verify(value: string, expectedHash: string, secret: string): Promise<boolean> {
const key = await getImportKey(secret, ["verify"]);
const encoder = new TextEncoder();
const valueData = encoder.encode(value);
const signature = hexToBuffer(expectedHash);
return crypto.subtle.verify("HMAC", key, signature, valueData);
}
/*
|--------------------------------------------------------------------------------
| Utilities
|--------------------------------------------------------------------------------
*/
async function getImportKey(secret: string, usages: KeyUsage[]): Promise<CryptoKey> {
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
return crypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: { name: "SHA-256" } }, false, usages);
}
function bufferToHex(buffer: ArrayBuffer): string {
return Array.from(new Uint8Array(buffer))
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
function hexToBuffer(hex: string): ArrayBuffer {
const bytes = new Uint8Array(hex.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)));
return bytes.buffer;
}

134
platform/vault/key-pair.ts Normal file
View File

@@ -0,0 +1,134 @@
import * as Jose from "jose";
export class KeyPair {
readonly #public: PublicKey;
readonly #private: PrivateKey;
readonly #algorithm: string;
constructor({ publicKey, privateKey }: Jose.GenerateKeyPairResult, algorithm: string) {
this.#public = new PublicKey(publicKey);
this.#private = new PrivateKey(privateKey);
this.#algorithm = algorithm;
}
get public() {
return this.#public;
}
get private() {
return this.#private;
}
get algorithm() {
return this.#algorithm;
}
async toJSON() {
return {
publicKey: await this.public.toString(),
privateKey: await this.private.toString(),
};
}
}
export class PublicKey {
readonly #key: Jose.CryptoKey;
constructor(key: Jose.CryptoKey) {
this.#key = key;
}
get key(): Jose.CryptoKey {
return this.#key;
}
async toString() {
return Jose.exportSPKI(this.#key);
}
}
export class PrivateKey {
readonly #key: Jose.CryptoKey;
constructor(key: Jose.CryptoKey) {
this.#key = key;
}
get key(): Jose.CryptoKey {
return this.#key;
}
async toString() {
return Jose.exportPKCS8(this.#key);
}
}
/*
|--------------------------------------------------------------------------------
| Factories
|--------------------------------------------------------------------------------
*/
/**
* Create a new key pair using the provided algorithm.
*
* @param algorithm - Algorithm to use for key generation.
*
* @returns new key pair instance
*/
export async function createKeyPair(algorithm: string): Promise<KeyPair> {
return new KeyPair(await Jose.generateKeyPair(algorithm, { extractable: true }), algorithm);
}
/**
* Loads a keypair from a previously exported keypair into a new KeyPair instance.
*
* @param keyPair - KeyPair to load into a new keyPair instance.
* @param algorithm - Algorithm to use for key generation.
*
* @returns new key pair instance
*/
export async function loadKeyPair({ publicKey, privateKey }: ExportedKeyPair, algorithm: string): Promise<KeyPair> {
return new KeyPair(
{
publicKey: await importPublicKey(publicKey, algorithm),
privateKey: await importPrivateKey(privateKey, algorithm),
},
algorithm,
);
}
/**
* Get a new Jose.KeyLike instance from a public key string.
*
* @param publicKey - Public key string.
* @param algorithm - Algorithm to used for key generation.
*
* @returns new Jose.KeyLike instance
*/
export async function importPublicKey(publicKey: string, algorithm: string): Promise<Jose.CryptoKey> {
return Jose.importSPKI(publicKey, algorithm, { extractable: true });
}
/**
* get a new Jose.KeyLike instance from a private key string.
*
* @param privateKey - Private key string.
* @param algorithm - Algorithm to used for key generation.
*
* @returns new Jose.KeyLike instance
*/
export async function importPrivateKey(privateKey: string, algorithm: string): Promise<Jose.CryptoKey> {
return Jose.importPKCS8(privateKey, algorithm, { extractable: true });
}
/*
|--------------------------------------------------------------------------------
| Types
|--------------------------------------------------------------------------------
*/
export type ExportedKeyPair = {
publicKey: string;
privateKey: string;
};

View File

@@ -0,0 +1,10 @@
{
"name": "@platform/vault",
"version": "0.0.0",
"private": true,
"type": "module",
"dependencies": {
"jose": "6.1.0",
"nanoid": "5.1.5"
}
}

84
platform/vault/vault.ts Normal file
View File

@@ -0,0 +1,84 @@
import * as Jose from "jose";
import { createKeyPair, ExportedKeyPair, importPrivateKey, importPublicKey, KeyPair, loadKeyPair } from "./key-pair.ts";
/*
|--------------------------------------------------------------------------------
| Security Settings
|--------------------------------------------------------------------------------
*/
const VAULT_ALGORITHM = "ECDH-ES+A256KW";
const VAULT_ENCRYPTION = "A256GCM";
/*
|--------------------------------------------------------------------------------
| Vault
|--------------------------------------------------------------------------------
*/
export class Vault {
#keyPair: KeyPair;
constructor(keyPair: KeyPair) {
this.#keyPair = keyPair;
}
get keys() {
return this.#keyPair;
}
/**
* Enecrypt the given value with the vaults key pair.
*
* @param value - Value to encrypt.
*/
async encrypt<T extends Record<string, unknown> | unknown[] | string>(value: T): Promise<string> {
const text = new TextEncoder().encode(JSON.stringify(value));
return new Jose.CompactEncrypt(text)
.setProtectedHeader({
alg: VAULT_ALGORITHM,
enc: VAULT_ENCRYPTION,
})
.encrypt(this.#keyPair.public.key);
}
/**
* Decrypts the given cypher text with the vaults key pair.
*
* @param cypherText - String to decrypt.
*/
async decrypt<T>(cypherText: string): Promise<T> {
const { plaintext } = await Jose.compactDecrypt(cypherText, this.#keyPair.private.key);
return JSON.parse(new TextDecoder().decode(plaintext));
}
}
/*
|--------------------------------------------------------------------------------
| Factories
|--------------------------------------------------------------------------------
*/
export async function createVault(): Promise<Vault> {
return new Vault(await createKeyPair(VAULT_ALGORITHM));
}
export async function importVault(keyPair: ExportedKeyPair): Promise<Vault> {
return new Vault(await loadKeyPair(keyPair, VAULT_ALGORITHM));
}
export async function encrypt<T extends Record<string, unknown> | unknown[] | string>(value: T, publicKey: string) {
const text = new TextEncoder().encode(JSON.stringify(value));
return new Jose.CompactEncrypt(text)
.setProtectedHeader({
alg: VAULT_ALGORITHM,
enc: VAULT_ENCRYPTION,
})
.encrypt(await importPublicKey(publicKey, VAULT_ALGORITHM));
}
export async function decrypt<T>(cypherText: string, privateKey: string): Promise<T> {
const { plaintext } = await Jose.compactDecrypt(cypherText, await importPrivateKey(privateKey, VAULT_ALGORITHM));
return JSON.parse(new TextDecoder().decode(plaintext));
}