feat: add application productivity foundations
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/encryption",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
@@ -23,6 +23,7 @@
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*"
|
||||
"@wrnexus/core": "workspace:*",
|
||||
"@wrnexus/db": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,3 +160,5 @@ export type {
|
||||
DecryptedHttpBody,
|
||||
ReplayStore,
|
||||
} from "./http.ts";
|
||||
export { defineSecretResource } from "./resource.ts";
|
||||
export type { SecretResourceOptions } from "./resource.ts";
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { Db } from "@wrnexus/db";
|
||||
import { decrypt, encrypt } from "./index.ts";
|
||||
|
||||
export interface SecretResourceOptions<T extends Record<string, unknown>> {
|
||||
db: () => Db;
|
||||
table: string;
|
||||
key: string | (() => string);
|
||||
encrypted: readonly (keyof T & string)[];
|
||||
hidden?: readonly (keyof T & string)[];
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const SAFE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||
|
||||
/** Encrypt selected database fields and guarantee hidden fields never leave `safe()`. */
|
||||
export function defineSecretResource<T extends Record<string, unknown>>(
|
||||
options: SecretResourceOptions<T>,
|
||||
) {
|
||||
const checked = (value: string) => {
|
||||
if (!SAFE.test(value)) throw new TypeError(`Unsafe secret resource identifier: ${value}`);
|
||||
return value;
|
||||
};
|
||||
const table = checked(options.table);
|
||||
const id = checked(options.id ?? "id");
|
||||
const encrypted = options.encrypted.map(checked);
|
||||
const hidden = new Set((options.hidden ?? options.encrypted).map(checked));
|
||||
const key = () => (typeof options.key === "function" ? options.key() : options.key);
|
||||
return {
|
||||
async seal(input: T): Promise<T> {
|
||||
const output = { ...input };
|
||||
const writable = output as Record<string, unknown>;
|
||||
for (const field of encrypted)
|
||||
if (writable[field] !== undefined)
|
||||
writable[field] = await encrypt(String(writable[field]), key());
|
||||
return output;
|
||||
},
|
||||
async open(input: T): Promise<T> {
|
||||
const output = { ...input };
|
||||
const writable = output as Record<string, unknown>;
|
||||
for (const field of encrypted)
|
||||
if (writable[field]) writable[field] = await decrypt(String(writable[field]), key());
|
||||
return output;
|
||||
},
|
||||
safe(input: T): Partial<T> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(input).filter(([field]) => !hidden.has(field)),
|
||||
) as Partial<T>;
|
||||
},
|
||||
async find(value: string | number): Promise<T | null> {
|
||||
return options.db().one<T>(`SELECT * FROM ${table} WHERE ${id} = ?`, [value]);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user