# @nice-code/util

Latest version **0.97.0** (published 2026-09-18) · 0 weekly downloads

## Install

```sh
npm install @nice-code/util
pnpm add @nice-code/util
yarn add @nice-code/util
bun add @nice-code/util
```

## Health

**Score 65/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.97.0 |
| Published | 2026-09-18 |
| First published | 2026-05-25 |
| Weekly downloads | 0 |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 4 |
| Unpacked size | 574.5 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | lostpebble |

## Links

- npm: https://www.npmjs.com/package/@nice-code/util
- Homepage: https://nicecode.io
- npm.io page: https://npm.io/package/@nice-code/util

## Dependencies (4)

- [nanoid](https://npm.io/package/nanoid.md) ^5.1.16
- [valibot](https://npm.io/package/valibot.md) ^1.4.2
- [@scure/base](https://npm.io/package/@scure/base.md) ^2.2.0
- [@standard-schema/spec](https://npm.io/package/@standard-schema/spec.md) ^1.1.0

## Recent versions

- 0.97.0 (latest) — 2026-09-18
- 0.96.0 — 2026-09-18
- 0.95.0 — 2026-09-09
- 0.94.0 — 2026-09-09
- 0.93.0 — 2026-09-09
- 0.92.0 — 2026-09-07
- 0.91.0 — 2026-09-02
- 0.90.0 — 2026-08-31
- 0.89.0 — 2026-08-25
- 0.87.0 — 2026-08-25
- 0.86.0 — 2026-08-21
- 0.85.0 — 2026-08-21
- 0.84.0 — 2026-08-21
- 0.83.0 — 2026-08-21
- 0.82.0 — 2026-08-20
- … 117 more at https://npm.io/package/@nice-code/util/versions

## README

# @nice-code/util

> **Docs:** [nicecode.io](https://nicecode.io) — guides, integrations, and the full API surface.
> **Working with an AI assistant?** Point it at [nicecode.io/llms-util.txt](https://nicecode.io/llms-util.txt) (just this package) or [nicecode.io/llms.txt](https://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

```bash
bun add @nice-code/util
```

---

## Typed storage

`ITypedStorage<T>` gives you fully typed, async, key-prefixed storage over any backend.

```ts
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 written
```

Optionally pass a `schemas` map (any Standard Schema library) to validate schema'd keys
**fail-closed** on every read, default, write, and both sides of an update — the schema's output is
what's returned and persisted, failures throw `StorageValidationError`, and keys without a schema
keep the blind-cast behavior. The full contract (sync-only validation, `onInvalid` observation
hook, versioned-durable-state pattern) is on the docs site's Typed Storage page.

### Adapters

```ts
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

```ts
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, …):

```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<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 and `withKeyPrefix(prefix)` for
a child namespace. Child adapters inherit `trackKeysForClearing`; an untracked parent never silently
re-enables the `__usedKeys__` index.

---

## Crypto

WebCrypto-based helpers — work in browsers, Workers / Durable Objects, Bun, and Node.

### Canonical JSON + SHA-256

Deterministic serialization + hashing for content hashes, idempotency keys, and signed challenges.
`stringifyCanonicalJson` makes equal data produce equal bytes (keys sorted by UTF-16 code unit,
`undefined` entries dropped, `toJSON` ignored, non-finite/bigint/cycles rejected — an exact,
frozen-vector-tested contract, deliberately *not* RFC 8785); `sha256Hex` / `sha256Base64` are
synchronous SHA-256 over strings or bytes; `hashCanonicalJsonSha256Hex` / `…Base64` combine them.

```ts
import { hashCanonicalJsonSha256Hex, stringifyCanonicalJson } from "@nice-code/util";

stringifyCanonicalJson({ b: 1, a: 2 }); // '{"a":2,"b":1}'
const key = hashCanonicalJsonSha256Hex({ op: "transfer", amount: 5 });
```

No built-in size/depth ceiling — bound untrusted input before canonicalizing. Full rules on the
JSDoc and the docs site.

### Canonical challenges

Domain-tagged, versioned, canonical-JSON signing challenges — the safe replacement for joining
challenge parts with `"::"` (which lets `["a::b"]` and `["a","b"]` sign identical bytes).
`buildCanonicalChallenge({ domainTag, version, fields })` defines the bytes;
`ClientCryptoKeyLink.signChallengeCanonical` / `verifyChallengeCanonicalFromLinkedClient` sign and
verify the same structured value on both ends. Strict inputs only — `undefined` entries, sparse
arrays, and non-plain objects are rejected, not normalized. The old `signChallenge` surface is
unchanged for deployed contracts.

### 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::<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:

```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 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`.

```ts
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, reset
```

> Crypto helpers require the `@scure/base` peer dependency.

---

## TypeScript utilities

```ts
import type { StringKeys } from "@nice-code/util";

// Extracts string keys from a type
type Keys = StringKeys<{ a: string; b: number; 0: boolean }>;
// → "a" | "b"
```

---
_Source: https://npm.io/package/@nice-code/util · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
