@nice-code/util 0.5.5 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +165 -73
  2. package/build/index.js +839 -7
  3. package/build/types/core/core_valibot_schemas.d.ts +13 -0
  4. package/build/types/core/createDataStringConverter_stringToObject.d.ts +12 -0
  5. package/build/types/crypto/aes_gcm/createAesGcmKeyFromX25519Keys.d.ts +6 -0
  6. package/build/types/crypto/aes_gcm/decryptBytesWithAesGcmKey.d.ts +9 -0
  7. package/build/types/crypto/aes_gcm/decryptTextDataWithAesGcmKey.d.ts +5 -0
  8. package/build/types/crypto/aes_gcm/encryptBytesWithAesGcmKey.d.ts +10 -0
  9. package/build/types/crypto/aes_gcm/encryptTextDataWithAesGcmKey.d.ts +5 -0
  10. package/build/types/crypto/client_key_link/ClientCryptoKeyLink.d.ts +181 -0
  11. package/build/types/crypto/client_key_link/buildVerifyKeyBoundInfoString.d.ts +20 -0
  12. package/build/types/crypto/crypto.converters.d.ts +53 -0
  13. package/build/types/crypto/crypto.schema.d.ts +92 -0
  14. package/build/types/crypto/ed25519/generateEd25519KeyPair.d.ts +1 -0
  15. package/build/types/crypto/ed25519/importEd25519Key.d.ts +35 -0
  16. package/build/types/crypto/ed25519/serializeEd25519Key_Jwk.d.ts +2 -0
  17. package/build/types/crypto/ed25519/serializeEd25519Key_Raw.d.ts +2 -0
  18. package/build/types/crypto/ed25519/signCombinedTextDataWithKeyEd25519.d.ts +2 -0
  19. package/build/types/crypto/ed25519/signTextDataWithKeyEd25519.d.ts +1 -0
  20. package/build/types/crypto/ed25519/verifyWithKeyEd25519.d.ts +5 -0
  21. package/build/types/crypto/index.d.ts +21 -0
  22. package/build/types/crypto/x25519/createSharedBitsFromX25519.d.ts +4 -0
  23. package/build/types/crypto/x25519/generateX25519KeyPair.d.ts +1 -0
  24. package/build/types/crypto/x25519/importX25519Key.d.ts +35 -0
  25. package/build/types/crypto/x25519/serializeX25519Key_Jwk.d.ts +2 -0
  26. package/build/types/crypto/x25519/serializeX25519Key_Raw.d.ts +2 -0
  27. package/build/types/data_type/index.d.ts +1 -0
  28. package/build/types/data_type/string/nullEmpty.d.ts +3 -0
  29. package/build/types/index.d.ts +2 -0
  30. package/package.json +5 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @nice-code/util
2
2
 
3
- Typed storage adapters for browser, Cloudflare Durable Objects, and in-memory use.
3
+ Typed storage adapters (browser, Cloudflare Durable Objects, in-memory) and WebCrypto utilities (Ed25519 signing, X25519 key exchange, AES-GCM encryption).
4
4
 
5
5
  ## Install
6
6
 
@@ -12,140 +12,232 @@ bun add @nice-code/util
12
12
 
13
13
  ## Typed storage
14
14
 
15
- `createTypedStorage` wraps a `StorageAdapter` and adds full TypeScript key and value inference based on a schema type parameter.
15
+ `ITypedStorage<T>` gives you fully typed, async, key-prefixed storage over any backend.
16
16
 
17
17
  ```ts
18
18
  import { createTypedWebLocalStorage } from "@nice-code/util";
19
19
 
20
- // Define the shape of your storage
21
20
  interface IAppStorage {
22
21
  user_id: string;
23
22
  theme: "light" | "dark";
24
- last_seen: number;
23
+ recent_searches: string[];
25
24
  }
26
25
 
27
- const storage = createTypedWebLocalStorage<IAppStorage>(localStorage, "app:");
26
+ const storage = createTypedWebLocalStorage<IAppStorage>({
27
+ localStorage,
28
+ keyPrefix: "app:",
29
+ });
28
30
 
29
31
  // All keys autocomplete; values are typed
30
32
  await storage.setJson("theme", "dark");
31
- const theme = await storage.getJson("theme"); // "light" | "dark" | undefined
33
+ const theme = await storage.getJson("theme"); // "light" | "dark" | undefined
32
34
  const userId = await storage.getJsonOrDef("user_id", "guest"); // string
33
- await storage.removeItem("last_seen");
34
- ```
35
35
 
36
- All methods are async and key-prefixed if a prefix is provided.
37
-
38
- ---
36
+ // Read-modify-write in one call
37
+ await storage.updateJsonWithDef("recent_searches", [], (cur) => [...cur, "query"]);
39
38
 
40
- ## Available adapters
39
+ await storage.removeItem("theme");
40
+ await storage.clearAll(); // removes only keys this storage has written
41
+ ```
41
42
 
42
- ### Browser — `localStorage` / `sessionStorage`
43
+ ### Adapters
43
44
 
44
45
  ```ts
45
46
  import {
46
47
  createTypedWebLocalStorage,
47
48
  createTypedWebSessionStorage,
49
+ createDurableObjectTypedStorage,
50
+ createTypedMemoryStorage_string,
51
+ createTypedMemoryStorage_json,
48
52
  } from "@nice-code/util";
49
53
 
50
- const local = createTypedWebLocalStorage<IAppStorage>(localStorage, "prefix:");
51
- const session = createTypedWebSessionStorage<IAppStorage>(sessionStorage);
52
- ```
54
+ // Browser
55
+ const local = createTypedWebLocalStorage<IAppStorage>({ localStorage, keyPrefix: "app:" });
56
+ const session = createTypedWebSessionStorage<IAppStorage>({ sessionStorage });
53
57
 
54
- ### Cloudflare Durable Objects
58
+ // Cloudflare Durable Objects (inside a DO class)
59
+ const doStorage = createDurableObjectTypedStorage<IDOStorage>({
60
+ durableObjectStorage: ctx.storage,
61
+ keyPrefix: "do:",
62
+ });
55
63
 
56
- ```ts
57
- import { createDurableObjectTypedStorage } from "@nice-code/util";
64
+ // In-memory (testing / SSR) — string-serialized or JSON-native
65
+ const mem = createTypedMemoryStorage_string<IAppStorage>();
66
+ const memJson = createTypedMemoryStorage_json<IAppStorage>();
67
+
68
+ // Share state between instances by passing the same Map
69
+ const shared = new Map<string, string>();
70
+ const a = createTypedMemoryStorage_string<IAppStorage>({ memoryStorageMap: shared });
71
+ const b = createTypedMemoryStorage_string<IAppStorage>({ memoryStorageMap: shared });
72
+ ```
58
73
 
59
- // Inside a Durable Object class
60
- export class MyDO {
61
- private storage: ITypedStorage<IDOStorage>;
74
+ ### `ITypedStorage<T>` interface
62
75
 
63
- constructor(state: DurableObjectState) {
64
- this.storage = createDurableObjectTypedStorage<IDOStorage>(state.storage, "do:");
65
- }
76
+ ```ts
77
+ interface ITypedStorage<T extends Record<string, any>> {
78
+ getJson<K>(key: K): Promise<T[K] | undefined>;
79
+ getJsonOrDef<K>(key: K, defVal: T[K]): Promise<T[K]>;
80
+ setJson<K>(key: K, val: T[K]): Promise<void>;
81
+ updateJson<K>(key: K, updater: (cur: T[K] | undefined) => T[K]): Promise<void>;
82
+ updateJsonWithDef<K>(key: K, defVal: T[K], updater: (cur: T[K]) => T[K]): Promise<void>;
83
+ removeItem<K>(key: K): Promise<void>;
84
+ clearAll(): Promise<void>;
66
85
  }
67
86
  ```
68
87
 
69
- ### In-memory (testing / SSR)
88
+ ### Custom backend
89
+
90
+ Implement the methods interface to wrap any storage (Redis, KV, …):
70
91
 
71
92
  ```ts
72
93
  import {
73
- createTypedMemoryStorage_string,
74
- createTypedMemoryStorage_json,
94
+ createTypedStorage,
95
+ EStorageAdapterType,
96
+ StorageAdapter,
97
+ type IStorageAdapterMethods_String,
75
98
  } from "@nice-code/util";
76
99
 
77
- // String-serialized (like localStorage)
78
- const store = createTypedMemoryStorage_string<IAppStorage>();
79
-
80
- // JSON-native (no serialization overhead)
81
- const jsonStore = createTypedMemoryStorage_json<IAppStorage>();
100
+ const redisMethods: IStorageAdapterMethods_String = {
101
+ type: EStorageAdapterType.string,
102
+ getItem: async (key) => redis.get(key),
103
+ setItem: async (key, value) => { await redis.set(key, value); },
104
+ removeItem: async (key) => { await redis.del(key); },
105
+ };
82
106
 
83
- // Pass in your own Map to share state between instances
84
- const shared = new Map<string, string>();
85
- const a = createTypedMemoryStorage_string<IAppStorage>(shared);
86
- const b = createTypedMemoryStorage_string<IAppStorage>(shared);
107
+ const storage = createTypedStorage<IMySchema>({
108
+ storageAdapter: new StorageAdapter({ methods: redisMethods, keyPrefix: "app:" }),
109
+ });
87
110
  ```
88
111
 
112
+ The lower-level `StorageAdapter` can also be used directly (untyped keys), including
113
+ `createJsonGetterSetter<T>(key)` for a single-key `{ get, set }` pair.
114
+
89
115
  ---
90
116
 
91
- ## `ITypedStorage<T>` interface
117
+ ## Crypto
118
+
119
+ WebCrypto-based helpers — work in browsers, Workers / Durable Objects, Bun, and Node.
120
+
121
+ ### Ed25519 — sign & verify
92
122
 
93
123
  ```ts
94
- interface ITypedStorage<T extends Record<string, any>> {
95
- getJson<K extends keyof T & string>(key: K): Promise<T[K] | undefined>;
96
- getJsonOrDef<K extends keyof T & string>(key: K, defVal: T[K]): Promise<T[K]>;
97
- setJson<K extends keyof T & string>(key: K, val: T[K]): Promise<void>;
98
- removeItem<K extends keyof T & string>(key: K): Promise<void>;
99
- }
124
+ import {
125
+ generateEd25519KeyPair,
126
+ importEd25519Key,
127
+ serializeEd25519Key_Raw,
128
+ signTextDataWithKeyEd25519,
129
+ verifyWithKeyEd25519,
130
+ } from "@nice-code/util";
131
+ import { base64 } from "@scure/base";
132
+
133
+ const keyPair = await generateEd25519KeyPair();
134
+
135
+ // Sign
136
+ const signature = await signTextDataWithKeyEd25519("challenge-text", keyPair.privateKey);
137
+ const signatureBase64 = base64.encode(signature);
138
+
139
+ // Serialize the public key for transport — "ed25519::raw_base64::<data>"
140
+ const { prefixed } = await serializeEd25519Key_Raw(keyPair.publicKey);
141
+
142
+ // Other side: import + verify
143
+ const publicKey = await importEd25519Key.public.fromFormattedString.extractable(prefixed);
144
+ const isValid = await verifyWithKeyEd25519({
145
+ challenge: "challenge-text",
146
+ signatureBase64,
147
+ publicKey,
148
+ });
100
149
  ```
101
150
 
102
- ---
151
+ Keys serialize to self-describing prefixed strings (`<algo>::<format>::<data>`), so a stored or
152
+ transported key always knows how to re-import itself. Private keys serialize via
153
+ `serializeEd25519Key_Jwk` / `serializeX25519Key_Jwk`; public keys via the `_Raw` variants.
103
154
 
104
- ## `StorageAdapter`low-level
155
+ ### X25519 + AES-GCM shared-key encryption
105
156
 
106
- Use `StorageAdapter` directly when you need a getter/setter pair or want to build a custom typed storage on top.
157
+ Derive a shared AES-GCM key from two X25519 key pairs (ECDH + HKDF), then encrypt/decrypt:
107
158
 
108
159
  ```ts
109
- import { StorageAdapter } from "@nice-code/util";
110
- import { createMemoryStorageMethods_string } from "@nice-code/util";
160
+ import {
161
+ generateX25519KeyPair,
162
+ createAesGcmKeyFromX25519Keys,
163
+ encryptTextDataWithAesGcmKey,
164
+ decryptTextDataWithAesGcmKey,
165
+ } from "@nice-code/util";
111
166
 
112
- const adapter = new StorageAdapter({
113
- methods: createMemoryStorageMethods_string(),
114
- keyPrefix: "myapp:",
167
+ const alice = await generateX25519KeyPair();
168
+ const bob = await generateX25519KeyPair();
169
+
170
+ // Both sides derive the same key from their private + the other's public key
171
+ const aliceKey = await createAesGcmKeyFromX25519Keys({
172
+ internalX25519PrivateKey: alice.privateKey,
173
+ externalX25519PublicKey: bob.publicKey,
174
+ saltString: "optional-session-salt",
175
+ infoString: "optional-context",
176
+ });
177
+ const bobKey = await createAesGcmKeyFromX25519Keys({
178
+ internalX25519PrivateKey: bob.privateKey,
179
+ externalX25519PublicKey: alice.publicKey,
180
+ saltString: "optional-session-salt",
181
+ infoString: "optional-context",
115
182
  });
116
183
 
117
- await adapter.setJson("token", { value: "abc", exp: 9999 });
118
- const token = await adapter.getJson<{ value: string; exp: number }>("token");
119
- const safe = await adapter.getJsonOrDef("token", { value: "", exp: 0 });
184
+ const payload = await encryptTextDataWithAesGcmKey({
185
+ aesGcmKey: aliceKey,
186
+ dataToEncrypt: "secret message",
187
+ }); // { nonce, ciphertext } — both base64
120
188
 
121
- // Convenient getter/setter pair for a single key
122
- const { get, set } = adapter.createJsonGetterSetter<string>("theme");
123
- await set("dark");
124
- const theme = await get(); // string | undefined
189
+ const plaintext = await decryptTextDataWithAesGcmKey({
190
+ aesGcmKey: bobKey,
191
+ dataToDecrypt: payload,
192
+ });
125
193
  ```
126
194
 
127
- ---
195
+ ### `ClientCryptoKeyLink` — full client-to-client crypto
128
196
 
129
- ## Custom adapter
130
-
131
- Implement `TStorageAdapterMethods` to wrap any storage backend:
197
+ High-level class managing a local identity (Ed25519 verify pair + X25519 exchange pair) and links
198
+ to other clients, with optional persistence through any `StorageAdapter`.
132
199
 
133
200
  ```ts
134
- import type { IStorageAdapterMethods_String } from "@nice-code/util";
135
- import { EStorageAdapterType } from "@nice-code/util";
201
+ import { ClientCryptoKeyLink, createMemoryStorageAdapter_json } from "@nice-code/util";
136
202
 
137
- const redisMethods: IStorageAdapterMethods_String = {
138
- type: EStorageAdapterType.string,
139
- getItem: async (key) => redis.get(key),
140
- setItem: async (key, value) => { await redis.set(key, value); },
141
- removeItem: async (key) => { await redis.del(key); },
142
- };
203
+ const link = new ClientCryptoKeyLink({
204
+ storageAdapter: createMemoryStorageAdapter_json(), // optional — omit for in-memory only
205
+ });
206
+ await link.initialize();
207
+
208
+ // Share these with the other side (serialized prefixed strings)
209
+ const { verifyPublicKey, exchangePublicKey } = await link.getLocalPublicKeys();
210
+
211
+ // Register the other side's keys
212
+ await link.linkClient({
213
+ linkedClientId: "client::partner-1",
214
+ verifyPublicKey: theirVerifyKey,
215
+ exchangePublicKey: theirExchangeKey,
216
+ // Optionally fold both verify keys into key derivation, so a
217
+ // tampered relayed key makes the first decryption fail:
218
+ bindVerifyKeysIntoDerivation: true,
219
+ });
220
+ // linkClientAndStore(...) persists the link across reloads
143
221
 
144
- const storage = createTypedStorage<IMySchema>({
145
- storageAdapter: new StorageAdapter({ methods: redisMethods, keyPrefix: "app:" }),
222
+ // Sign + encrypt for the linked client (shared key derived & cached automatically)
223
+ const { encryptedData, signatureBase64 } = await link.signAndEncryptDataForLinkedClient({
224
+ linkedClientId: "client::partner-1",
225
+ dataToEncrypt: "hello",
146
226
  });
227
+
228
+ // Other side: decrypt + verify in one call
229
+ const { data, isValid } = await otherLink.decryptAndVerifyDataFromLinkedClient({
230
+ linkedClientId: "client::me",
231
+ dataToDecrypt: encryptedData,
232
+ signatureBase64,
233
+ });
234
+
235
+ // Also: signChallenge, verifyChallengeFromLinkedClient, encryptDataForLinkedClient,
236
+ // decryptDataFromLinkedClient, unlinkClient, unlinkAllClients, reset
147
237
  ```
148
238
 
239
+ > Crypto helpers require the `@scure/base` peer dependency.
240
+
149
241
  ---
150
242
 
151
243
  ## TypeScript utilities