@nice-code/util
v0.63.0
Published
Readme
@nice-code/util
Docs: nicecode.io — guides, integrations, and the full API surface. Working with an AI assistant? Point it at nicecode.io/llms-util.txt (just this package) or nicecode.io/llms.txt (the whole stack) — the complete, current docs flattened into plain text.
Typed storage adapters (browser, Cloudflare Durable Objects, in-memory) and WebCrypto utilities (Ed25519 signing, X25519 key exchange, AES-GCM encryption).
Install
bun add @nice-code/utilTyped storage
ITypedStorage<T> gives you fully typed, async, key-prefixed storage over any backend.
import { createTypedWebLocalStorage } from "@nice-code/util";
interface IAppStorage {
user_id: string;
theme: "light" | "dark";
recent_searches: string[];
}
const storage = createTypedWebLocalStorage<IAppStorage>({
localStorage,
keyPrefix: "app:",
});
// All keys autocomplete; values are typed
await storage.setJson("theme", "dark");
const theme = await storage.getJson("theme"); // "light" | "dark" | undefined
const userId = await storage.getJsonOrDef("user_id", "guest"); // string
// Read-modify-write in one call
await storage.updateJsonWithDef("recent_searches", [], (cur) => [...cur, "query"]);
await storage.removeItem("theme");
await storage.clearAll(); // removes only keys this storage has writtenAdapters
import {
createTypedWebLocalStorage,
createTypedWebSessionStorage,
createDurableObjectTypedStorage,
createTypedMemoryStorage_string,
createTypedMemoryStorage_json,
} from "@nice-code/util";
// Browser
const local = createTypedWebLocalStorage<IAppStorage>({ localStorage, keyPrefix: "app:" });
const session = createTypedWebSessionStorage<IAppStorage>({ sessionStorage });
// Cloudflare Durable Objects (inside a DO class)
const doStorage = createDurableObjectTypedStorage<IDOStorage>({
durableObjectStorage: ctx.storage,
keyPrefix: "do:",
});
// In-memory (testing / SSR) — string-serialized or JSON-native
const mem = createTypedMemoryStorage_string<IAppStorage>();
const memJson = createTypedMemoryStorage_json<IAppStorage>();
// Share state between instances by passing the same Map
const shared = new Map<string, string>();
const a = createTypedMemoryStorage_string<IAppStorage>({ memoryStorageMap: shared });
const b = createTypedMemoryStorage_string<IAppStorage>({ memoryStorageMap: shared });ITypedStorage<T> interface
interface ITypedStorage<T extends Record<string, any>> {
getJson<K>(key: K): Promise<T[K] | undefined>;
getJsonOrDef<K>(key: K, defVal: T[K]): Promise<T[K]>;
setJson<K>(key: K, val: T[K]): Promise<void>;
updateJson<K>(key: K, updater: (cur: T[K] | undefined) => T[K]): Promise<void>;
updateJsonWithDef<K>(key: K, defVal: T[K], updater: (cur: T[K]) => T[K]): Promise<void>;
removeItem<K>(key: K): Promise<void>;
clearAll(): Promise<void>;
}Custom backend
Implement the methods interface to wrap any storage (Redis, KV, …):
import {
createTypedStorage,
EStorageAdapterType,
StorageAdapter,
type IStorageAdapterMethods_String,
} from "@nice-code/util";
const redisMethods: IStorageAdapterMethods_String = {
type: EStorageAdapterType.string,
getItem: async (key) => redis.get(key),
setItem: async (key, value) => { await redis.set(key, value); },
removeItem: async (key) => { await redis.del(key); },
};
const storage = createTypedStorage<IMySchema>({
storageAdapter: new StorageAdapter({ methods: redisMethods, keyPrefix: "app:" }),
});The lower-level StorageAdapter can also be used directly (untyped keys), including
createJsonGetterSetter<T>(key) for a single-key { get, set } pair.
Crypto
WebCrypto-based helpers — work in browsers, Workers / Durable Objects, Bun, and Node.
Ed25519 — sign & verify
import {
generateEd25519KeyPair,
importEd25519Key,
serializeEd25519Key_Raw,
signTextDataWithKeyEd25519,
verifyWithKeyEd25519,
} from "@nice-code/util";
import { base64 } from "@scure/base";
const keyPair = await generateEd25519KeyPair();
// Sign
const signature = await signTextDataWithKeyEd25519("challenge-text", keyPair.privateKey);
const signatureBase64 = base64.encode(signature);
// Serialize the public key for transport — "ed25519::raw_base64::<data>"
const { prefixed } = await serializeEd25519Key_Raw(keyPair.publicKey);
// Other side: import + verify
const publicKey = await importEd25519Key.public.fromFormattedString.extractable(prefixed);
const isValid = await verifyWithKeyEd25519({
challenge: "challenge-text",
signatureBase64,
publicKey,
});Keys serialize to self-describing prefixed strings (<algo>::<format>::<data>), so a stored or
transported key always knows how to re-import itself. Private keys serialize via
serializeEd25519Key_Jwk / serializeX25519Key_Jwk; public keys via the _Raw variants.
X25519 + AES-GCM — shared-key encryption
Derive a shared AES-GCM key from two X25519 key pairs (ECDH + HKDF), then encrypt/decrypt:
import {
generateX25519KeyPair,
createAesGcmKeyFromX25519Keys,
encryptTextDataWithAesGcmKey,
decryptTextDataWithAesGcmKey,
} from "@nice-code/util";
const alice = await generateX25519KeyPair();
const bob = await generateX25519KeyPair();
// Both sides derive the same key from their private + the other's public key
const aliceKey = await createAesGcmKeyFromX25519Keys({
internalX25519PrivateKey: alice.privateKey,
externalX25519PublicKey: bob.publicKey,
saltString: "optional-session-salt",
infoString: "optional-context",
});
const bobKey = await createAesGcmKeyFromX25519Keys({
internalX25519PrivateKey: bob.privateKey,
externalX25519PublicKey: alice.publicKey,
saltString: "optional-session-salt",
infoString: "optional-context",
});
const payload = await encryptTextDataWithAesGcmKey({
aesGcmKey: aliceKey,
dataToEncrypt: "secret message",
}); // { nonce, ciphertext } — both base64
const plaintext = await decryptTextDataWithAesGcmKey({
aesGcmKey: bobKey,
dataToDecrypt: payload,
});ClientCryptoKeyLink — full client-to-client crypto
High-level class managing a local identity (Ed25519 verify pair + X25519 exchange pair) and links
to other clients, with optional persistence through any StorageAdapter.
import { ClientCryptoKeyLink, createWebLocalStorageAdapter } from "@nice-code/util";
// Anything identity-bearing wants a DURABLE adapter: a peer pins this identity's verify key the
// first time it connects (trust-on-first-use), so an identity that regenerates on reload is
// rejected from the second load on. Memory adapters (`createMemoryStorageAdapter_json()`) are for
// tests — or omit `storageAdapter` entirely for a deliberately ephemeral, in-memory identity.
const link = new ClientCryptoKeyLink({
storageAdapter: createWebLocalStorageAdapter({ localStorage, keyPrefix: "crypto:" }),
});
await link.initialize();
// Share these with the other side (serialized prefixed strings)
const { verifyPublicKey, exchangePublicKey } = await link.getLocalPublicKeys();
// Register the other side's keys
await link.linkClient({
linkedClientId: "client::partner-1",
verifyPublicKey: theirVerifyKey,
exchangePublicKey: theirExchangeKey,
// Optionally fold both verify keys into key derivation, so a
// tampered relayed key makes the first decryption fail:
bindVerifyKeysIntoDerivation: true,
});
// linkClientAndStore(...) persists the link across reloads
// Sign + encrypt for the linked client (shared key derived & cached automatically)
const { encryptedData, signatureBase64 } = await link.signAndEncryptDataForLinkedClient({
linkedClientId: "client::partner-1",
dataToEncrypt: "hello",
});
// Other side: decrypt + verify in one call
const { data, isValid } = await otherLink.decryptAndVerifyDataFromLinkedClient({
linkedClientId: "client::me",
dataToDecrypt: encryptedData,
signatureBase64,
});
// Also: signChallenge, verifyChallengeFromLinkedClient, encryptDataForLinkedClient,
// decryptDataFromLinkedClient, unlinkClient, unlinkAllClients, resetCrypto helpers require the
@scure/basepeer dependency.
TypeScript utilities
import type { StringKeys } from "@nice-code/util";
// Extracts string keys from a type
type Keys = StringKeys<{ a: string; b: number; 0: boolean }>;
// → "a" | "b"