@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.
- package/README.md +165 -73
- package/build/index.js +839 -7
- package/build/types/core/core_valibot_schemas.d.ts +13 -0
- package/build/types/core/createDataStringConverter_stringToObject.d.ts +12 -0
- package/build/types/crypto/aes_gcm/createAesGcmKeyFromX25519Keys.d.ts +6 -0
- package/build/types/crypto/aes_gcm/decryptBytesWithAesGcmKey.d.ts +9 -0
- package/build/types/crypto/aes_gcm/decryptTextDataWithAesGcmKey.d.ts +5 -0
- package/build/types/crypto/aes_gcm/encryptBytesWithAesGcmKey.d.ts +10 -0
- package/build/types/crypto/aes_gcm/encryptTextDataWithAesGcmKey.d.ts +5 -0
- package/build/types/crypto/client_key_link/ClientCryptoKeyLink.d.ts +181 -0
- package/build/types/crypto/client_key_link/buildVerifyKeyBoundInfoString.d.ts +20 -0
- package/build/types/crypto/crypto.converters.d.ts +53 -0
- package/build/types/crypto/crypto.schema.d.ts +92 -0
- package/build/types/crypto/ed25519/generateEd25519KeyPair.d.ts +1 -0
- package/build/types/crypto/ed25519/importEd25519Key.d.ts +35 -0
- package/build/types/crypto/ed25519/serializeEd25519Key_Jwk.d.ts +2 -0
- package/build/types/crypto/ed25519/serializeEd25519Key_Raw.d.ts +2 -0
- package/build/types/crypto/ed25519/signCombinedTextDataWithKeyEd25519.d.ts +2 -0
- package/build/types/crypto/ed25519/signTextDataWithKeyEd25519.d.ts +1 -0
- package/build/types/crypto/ed25519/verifyWithKeyEd25519.d.ts +5 -0
- package/build/types/crypto/index.d.ts +21 -0
- package/build/types/crypto/x25519/createSharedBitsFromX25519.d.ts +4 -0
- package/build/types/crypto/x25519/generateX25519KeyPair.d.ts +1 -0
- package/build/types/crypto/x25519/importX25519Key.d.ts +35 -0
- package/build/types/crypto/x25519/serializeX25519Key_Jwk.d.ts +2 -0
- package/build/types/crypto/x25519/serializeX25519Key_Raw.d.ts +2 -0
- package/build/types/data_type/index.d.ts +1 -0
- package/build/types/data_type/string/nullEmpty.d.ts +3 -0
- package/build/types/index.d.ts +2 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @nice-code/util
|
|
2
2
|
|
|
3
|
-
Typed storage adapters
|
|
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
|
-
`
|
|
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
|
-
|
|
23
|
+
recent_searches: string[];
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
const storage = createTypedWebLocalStorage<IAppStorage>(
|
|
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");
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
---
|
|
36
|
+
// Read-modify-write in one call
|
|
37
|
+
await storage.updateJsonWithDef("recent_searches", [], (cur) => [...cur, "query"]);
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
await storage.removeItem("theme");
|
|
40
|
+
await storage.clearAll(); // removes only keys this storage has written
|
|
41
|
+
```
|
|
41
42
|
|
|
42
|
-
###
|
|
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
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
54
|
+
// Browser
|
|
55
|
+
const local = createTypedWebLocalStorage<IAppStorage>({ localStorage, keyPrefix: "app:" });
|
|
56
|
+
const session = createTypedWebSessionStorage<IAppStorage>({ sessionStorage });
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
// Cloudflare Durable Objects (inside a DO class)
|
|
59
|
+
const doStorage = createDurableObjectTypedStorage<IDOStorage>({
|
|
60
|
+
durableObjectStorage: ctx.storage,
|
|
61
|
+
keyPrefix: "do:",
|
|
62
|
+
});
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
export class MyDO {
|
|
61
|
-
private storage: ITypedStorage<IDOStorage>;
|
|
74
|
+
### `ITypedStorage<T>` interface
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
###
|
|
88
|
+
### Custom backend
|
|
89
|
+
|
|
90
|
+
Implement the methods interface to wrap any storage (Redis, KV, …):
|
|
70
91
|
|
|
71
92
|
```ts
|
|
72
93
|
import {
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
createTypedStorage,
|
|
95
|
+
EStorageAdapterType,
|
|
96
|
+
StorageAdapter,
|
|
97
|
+
type IStorageAdapterMethods_String,
|
|
75
98
|
} from "@nice-code/util";
|
|
76
99
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
155
|
+
### X25519 + AES-GCM — shared-key encryption
|
|
105
156
|
|
|
106
|
-
|
|
157
|
+
Derive a shared AES-GCM key from two X25519 key pairs (ECDH + HKDF), then encrypt/decrypt:
|
|
107
158
|
|
|
108
159
|
```ts
|
|
109
|
-
import {
|
|
110
|
-
|
|
160
|
+
import {
|
|
161
|
+
generateX25519KeyPair,
|
|
162
|
+
createAesGcmKeyFromX25519Keys,
|
|
163
|
+
encryptTextDataWithAesGcmKey,
|
|
164
|
+
decryptTextDataWithAesGcmKey,
|
|
165
|
+
} from "@nice-code/util";
|
|
111
166
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
184
|
+
const payload = await encryptTextDataWithAesGcmKey({
|
|
185
|
+
aesGcmKey: aliceKey,
|
|
186
|
+
dataToEncrypt: "secret message",
|
|
187
|
+
}); // { nonce, ciphertext } — both base64
|
|
120
188
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
|
135
|
-
import { EStorageAdapterType } from "@nice-code/util";
|
|
201
|
+
import { ClientCryptoKeyLink, createMemoryStorageAdapter_json } from "@nice-code/util";
|
|
136
202
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|