# @nice-code/util — documentation Typed storage adapters and crypto helpers. This file concatenates only the @nice-code/util pages of https://nicecode.io — point a local AI coding assistant at it when you're working with just this package. For the full set (every nice-code package and how they fit together) use /llms.txt. Generated from src/content/docs — do not edit by hand. --- # Crypto Source: /nice-util/crypto Description: WebCrypto helpers — Ed25519 signing, X25519 exchange, AES-GCM, and a client link. Crypto helpers built on WebCrypto, so they run the same in browsers, Workers / Durable Objects, Bun, and Node. When a key is turned into a string, that string carries its own description (`::::`) — so wherever you store or send a key, it always knows how to load itself back in. > Crypto helpers require the `@scure/base` peer dependency. ## Ed25519 — sign & verify ```ts 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::" 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, }); ``` ## X25519 + AES-GCM — shared-key encryption Derive a shared AES-GCM key from two X25519 key pairs (ECDH + HKDF), then encrypt/decrypt: ```ts 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 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 A high-level class that holds your local identity (an Ed25519 sign/verify pair plus an X25519 exchange pair) and the links to other clients. It can optionally save all of this through any `StorageAdapter`. ```ts import { ClientCryptoKeyLink, createMemoryStorageAdapter_json } from "@nice-code/util"; const link = new ClientCryptoKeyLink({ storageAdapter: createMemoryStorageAdapter_json(), // optional — omit for in-memory only }); 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, bindVerifyKeysIntoDerivation: true, // a tampered relayed key makes the first decryption fail }); // 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, }); ``` This is the same `ClientCryptoKeyLink` used across the stack to back the crypto identity of secure connections — including the `identityMode: "required"` provisioning described under [Security Levels](/nice-wire/security/#secure-http-needs-no-shared-memory-between-requests). ## TypeScript utilities ```ts import type { StringKeys } from "@nice-code/util"; type Keys = StringKeys<{ a: string; b: number; 0: boolean }>; // → "a" | "b" ``` --- # Typed Storage Source: /nice-util/storage Description: Fully typed, async, key-prefixed storage over any backend. `@nice-code/util` is the quiet foundation the rest of the stack stands on: one typed storage interface over any backend (browser, Durable Object, memory), plus the WebCrypto that authenticates every secure connection. You rarely import it directly — but a realm's identity persists through it, and a secure handshake is built on it. This page is the storage half; [Crypto](/nice-util/crypto/) is the other. ```bash bun add @nice-code/util ``` ## ITypedStorage `ITypedStorage` is a typed key/value store you can put on top of any backend. Every key and value is typed, every method is async, and all your keys get a shared prefix so they don't collide with anything else. ```ts import { createTypedWebLocalStorage } from "@nice-code/util"; interface IAppStorage { user_id: string; theme: "light" | "dark"; recent_searches: string[]; } const storage = createTypedWebLocalStorage({ 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 written ``` ## Adapters ```ts import { createTypedWebLocalStorage, createTypedWebSessionStorage, createDurableObjectTypedStorage, createTypedMemoryStorage_string, createTypedMemoryStorage_json, } from "@nice-code/util"; // Browser const local = createTypedWebLocalStorage({ localStorage, keyPrefix: "app:" }); const session = createTypedWebSessionStorage({ sessionStorage }); // Cloudflare Durable Objects (inside a DO class) const doStorage = createDurableObjectTypedStorage({ durableObjectStorage: ctx.storage, keyPrefix: "do:", }); // In-memory (testing / SSR) — string-serialized or JSON-native const mem = createTypedMemoryStorage_string(); const memJson = createTypedMemoryStorage_json(); // Share state between instances by passing the same Map const shared = new Map(); const a = createTypedMemoryStorage_string({ memoryStorageMap: shared }); const b = createTypedMemoryStorage_string({ memoryStorageMap: shared }); ``` ## The interface ```ts interface ITypedStorage> { getJson(key: K): Promise; getJsonOrDef(key: K, defVal: T[K]): Promise; setJson(key: K, val: T[K]): Promise; updateJson(key: K, updater: (cur: T[K] | undefined) => T[K]): Promise; updateJsonWithDef(key: K, defVal: T[K], updater: (cur: T[K]) => T[K]): Promise; removeItem(key: K): Promise; clearAll(): Promise; } ``` ## Wrapping your own backend Fill in the methods interface to put typed storage on top of any backend (Redis, KV, …): ```ts 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({ storageAdapter: new StorageAdapter({ methods: redisMethods, keyPrefix: "app:" }), }); ``` You can also use the lower-level `StorageAdapter` directly (with untyped keys), including `createJsonGetterSetter(key)` to get a simple `{ get, set }` pair for one key.