@mblaney/holster 1.0.34 → 1.0.36

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 (67) hide show
  1. package/build/holster.js +3 -3
  2. package/build/holster.js.map +3 -3
  3. package/dist/holster.js +4 -0
  4. package/dist/holster.js.map +7 -0
  5. package/package.json +23 -5
  6. package/src/get.js +11 -19
  7. package/src/ham.js +17 -47
  8. package/src/holster.js +106 -28
  9. package/src/sea.js +0 -52
  10. package/src/store.js +7 -7
  11. package/src/wire.js +8 -2
  12. package/ts/src/array.ts +78 -0
  13. package/ts/src/buffer.ts +441 -0
  14. package/ts/src/dup.ts +49 -0
  15. package/ts/src/get.ts +83 -0
  16. package/ts/src/ham.ts +254 -0
  17. package/ts/src/holster.ts +931 -0
  18. package/ts/src/index.ts +35 -0
  19. package/ts/src/radisk.ts +585 -0
  20. package/ts/src/radix.ts +157 -0
  21. package/ts/src/schemas.ts +519 -0
  22. package/ts/src/sea-utils.ts +103 -0
  23. package/ts/src/sea.ts +388 -0
  24. package/ts/src/store.ts +321 -0
  25. package/ts/src/user.ts +381 -0
  26. package/ts/src/utils.ts +232 -0
  27. package/ts/src/wire.ts +829 -0
  28. package/.dockerignore +0 -9
  29. package/.github/workflows/node.js.yml +0 -31
  30. package/.github/workflows/npm-publish.yml +0 -33
  31. package/Dockerfile +0 -20
  32. package/examples/index.html +0 -33
  33. package/examples/index.js +0 -49
  34. package/examples/user.html +0 -31
  35. package/examples/user.js +0 -93
  36. package/examples/wire.html +0 -27
  37. package/examples/wire.js +0 -55
  38. package/test/holster.get.test.js +0 -94
  39. package/test/holster.lex.test.js +0 -196
  40. package/test/holster.off.test.js +0 -53
  41. package/test/holster.on.test.js +0 -264
  42. package/test/holster.put.test.js +0 -358
  43. package/test/holster.secure.test.js +0 -202
  44. package/test/holster.user.get.test.js +0 -114
  45. package/test/holster.user.lex.test.js +0 -149
  46. package/test/holster.user.off.test.js +0 -73
  47. package/test/holster.user.on.test.js +0 -300
  48. package/test/holster.user.put.test.js +0 -463
  49. package/test/kitty/! +0 -9
  50. package/test/names.js +0 -225
  51. package/test/radisk.cache.test.js +0 -213
  52. package/test/radisk.test.js +0 -331
  53. package/test/radix.test.js +0 -117
  54. package/test/sea.test.js +0 -284
  55. package/test/split.test.js +0 -317
  56. package/test/store.test.js +0 -316
  57. package/test/system/basic.test.js +0 -29
  58. package/test/system/chained-on.test.js +0 -41
  59. package/test/system/concurrent-listeners.test.js +0 -125
  60. package/test/system/immediate-read.test.js +0 -30
  61. package/test/system/listener-before-data.test.js +0 -51
  62. package/test/system/promise-wrapper.test.js +0 -47
  63. package/test/system/user-chained-on.test.js +0 -60
  64. package/test/system/user-concurrent-listeners.test.js +0 -142
  65. package/test/system/user-listener-before-data.test.js +0 -66
  66. package/test/user.test.js +0 -173
  67. package/test/wire.test.js +0 -158
@@ -0,0 +1,103 @@
1
+ /**
2
+ * SEA Utilities - Cryptographic utility functions
3
+ * Provides helpers for encryption, hashing, and key management
4
+ */
5
+
6
+ import SafeBuffer from "./buffer.ts"
7
+ import type { JWK } from "./schemas.ts"
8
+
9
+ const isNode = typeof document === "undefined"
10
+
11
+ // Get crypto based on environment - dynamic import for Node.js compatibility
12
+ const crypto: Crypto = isNode
13
+ ? ((await import(/*webpackIgnore: true*/ "node:crypto")).webcrypto as Crypto)
14
+ : globalThis.crypto
15
+
16
+ export const subtle = crypto.subtle
17
+
18
+ /**
19
+ * Convert data to string format for cryptographic operations
20
+ */
21
+ export const stringify = (data: unknown): string => {
22
+ return typeof data === "string" ? data : JSON.stringify(data)
23
+ }
24
+
25
+ /**
26
+ * Parse JSON text safely
27
+ */
28
+ export const parse = (text: string): unknown => {
29
+ try {
30
+ return JSON.parse(text)
31
+ } catch {
32
+ return text
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Generate cryptographically secure random bytes
38
+ */
39
+ export const random = (length: number): SeaArray => {
40
+ const array = new Uint8Array(length)
41
+ const filled = crypto.getRandomValues(array)
42
+ return SafeBuffer.from(filled)
43
+ }
44
+
45
+ /**
46
+ * Create JWK from public and optional private key components
47
+ */
48
+ export const jwk = (pub: string, priv?: string): JWK => {
49
+ const [x, y] = pub.split(".")
50
+ const ops = priv ? ["sign"] : ["verify"]
51
+ return {
52
+ kty: "EC",
53
+ crv: "P-256",
54
+ x: x!,
55
+ y: y!,
56
+ d: priv,
57
+ ext: true,
58
+ key_ops: ops,
59
+ }
60
+ }
61
+
62
+ /**
63
+ * SHA-256 hash of data
64
+ */
65
+ export const sha256 = async (data: unknown): Promise<SeaArray> => {
66
+ const hash = await subtle.digest(
67
+ { name: "SHA-256" },
68
+ new TextEncoder().encode(stringify(data))
69
+ )
70
+ return SafeBuffer.from(new Uint8Array(hash))
71
+ }
72
+
73
+ /**
74
+ * Derive AES key from password and salt
75
+ */
76
+ export const aeskey = async (
77
+ key: string,
78
+ salt: SeaArray
79
+ ) => {
80
+ const combined = key + salt.toString("utf8")
81
+ const hashResult = await sha256(combined)
82
+ const jwkKey = keyToJwk(hashResult)
83
+ return await subtle.importKey("jwk", jwkKey, { name: "AES-GCM" }, false, [
84
+ "encrypt",
85
+ "decrypt",
86
+ ])
87
+ }
88
+
89
+ /**
90
+ * Convert key buffer to JWK format for AES
91
+ */
92
+ const keyToJwk = (key: SeaArray): JWK => {
93
+ const k = key
94
+ .toString("base64")
95
+ .replace(/\+/g, "-")
96
+ .replace(/\//g, "_")
97
+ .replace(/=/g, "")
98
+ return { kty: "oct", k: k, ext: false, alg: "A256GCM" }
99
+ }
100
+
101
+ // Re-export SeaArray type for convenience
102
+ import type SeaArray from "./array.ts"
103
+
package/ts/src/sea.ts ADDED
@@ -0,0 +1,388 @@
1
+ /**
2
+ * SEA - Security, Encryption, and Authorization
3
+ * Cryptographic functions for signing, verification, encryption, and decryption
4
+ */
5
+
6
+ import { userPublicKey, userSignature } from "./utils.ts"
7
+ import * as utils from "./sea-utils.ts"
8
+ import SafeBuffer from "./buffer.ts"
9
+ import type {
10
+ UserPair,
11
+ EncryptedData,
12
+ SignedData,
13
+ } from "./schemas.ts"
14
+
15
+ type Callback<T> = (result: T) => void
16
+
17
+ /**
18
+ * SEA cryptographic operations
19
+ */
20
+ const SEA = {
21
+ /**
22
+ * Generate a new ECDSA + ECDH key pair
23
+ */
24
+ pair: async (cb?: Callback<UserPair>): Promise<UserPair> => {
25
+ // ECDSA keys for signing/verifying
26
+ const ecdsa = await utils.subtle
27
+ .generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [
28
+ "sign",
29
+ "verify",
30
+ ])
31
+ .then(async keys => {
32
+ const pub = await utils.subtle.exportKey("jwk", keys.publicKey)
33
+ return {
34
+ priv: (await utils.subtle.exportKey("jwk", keys.privateKey)).d!,
35
+ pub: pub.x! + "." + pub.y!,
36
+ }
37
+ })
38
+
39
+ // ECDH keys for encryption/decryption
40
+ const ecdh = await utils.subtle
41
+ .generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveKey"])
42
+ .then(async keys => {
43
+ const pub = await utils.subtle.exportKey("jwk", keys.publicKey)
44
+ return {
45
+ epriv: (await utils.subtle.exportKey("jwk", keys.privateKey)).d!,
46
+ epub: pub.x! + "." + pub.y!,
47
+ }
48
+ })
49
+
50
+ const pair: UserPair = {
51
+ pub: ecdsa.pub,
52
+ priv: ecdsa.priv,
53
+ epub: ecdh.epub,
54
+ epriv: ecdh.epriv,
55
+ }
56
+ if (cb) cb(pair)
57
+ return pair
58
+ },
59
+
60
+ /**
61
+ * Encrypt data with a key pair
62
+ */
63
+ encrypt: async (
64
+ data: unknown,
65
+ pair: Partial<UserPair> | null,
66
+ cb?: Callback<EncryptedData | null>
67
+ ): Promise<EncryptedData | null> => {
68
+ if (!pair || !pair.epriv) {
69
+ if (cb) cb(null)
70
+ return null
71
+ }
72
+
73
+ const rand = { s: utils.random(9), iv: utils.random(15) }
74
+ const ct = await utils.aeskey(pair.epriv, rand.s).then(aes => {
75
+ return utils.subtle.encrypt(
76
+ {
77
+ name: "AES-GCM",
78
+ iv: new Uint8Array(rand.iv),
79
+ },
80
+ aes,
81
+ new TextEncoder().encode(utils.stringify(data))
82
+ )
83
+ })
84
+ const enc: EncryptedData = {
85
+ ct: SafeBuffer.from(ct, "binary").toString("base64"),
86
+ iv: rand.iv.toString("base64"),
87
+ s: rand.s.toString("base64"),
88
+ }
89
+ if (cb) cb(enc)
90
+ return enc
91
+ },
92
+
93
+ /**
94
+ * Decrypt data with a key pair
95
+ */
96
+ decrypt: async (
97
+ enc: Partial<EncryptedData> | null,
98
+ pair: Partial<UserPair> | null,
99
+ cb?: Callback<unknown | null>
100
+ ): Promise<unknown | null> => {
101
+ if (!enc || !enc.ct || !enc.iv || !enc.s || !pair || !pair.epriv) {
102
+ if (cb) cb(null)
103
+ return null
104
+ }
105
+
106
+ const data = {
107
+ ct: SafeBuffer.from(enc.ct, "base64"),
108
+ iv: SafeBuffer.from(enc.iv, "base64"),
109
+ s: SafeBuffer.from(enc.s, "base64"),
110
+ }
111
+ try {
112
+ const ct = await utils.aeskey(pair.epriv, data.s).then(aes => {
113
+ return utils.subtle.decrypt(
114
+ {
115
+ name: "AES-GCM",
116
+ iv: new Uint8Array(data.iv),
117
+ tagLength: 128,
118
+ },
119
+ aes,
120
+ new Uint8Array(data.ct)
121
+ )
122
+ })
123
+ const dec = utils.parse(new TextDecoder("utf8").decode(ct))
124
+ if (cb) cb(dec)
125
+ return dec
126
+ } catch (err) {
127
+ // An error will be thrown if the wrong key is used
128
+ if (cb) cb(null)
129
+ return null
130
+ }
131
+ },
132
+
133
+ /**
134
+ * Verify a signed message
135
+ */
136
+ verify: async (
137
+ data: string | SignedData,
138
+ pair: Partial<UserPair> | null,
139
+ cb?: Callback<unknown | null>
140
+ ): Promise<unknown | null> => {
141
+ if (!pair || !pair.pub) {
142
+ if (cb) cb(null)
143
+ return null
144
+ }
145
+
146
+ const signed = (
147
+ typeof data === "string" ? utils.parse(data) : data
148
+ ) as SignedData
149
+ const key = await utils.subtle.importKey(
150
+ "jwk",
151
+ utils.jwk(pair.pub),
152
+ { name: "ECDSA", namedCurve: "P-256" },
153
+ false,
154
+ ["verify"]
155
+ )
156
+
157
+ let msg: string | Record<string, unknown> = {}
158
+ if (typeof signed.m === "string") {
159
+ msg = signed.m
160
+ } else {
161
+ // Allow data to be passed in with graph meta data,
162
+ // (which should not be part of signature, so not verified)
163
+ for (const k of Object.keys(signed.m).sort()) {
164
+ if (k !== "_" && k !== userPublicKey && k !== userSignature) {
165
+ (msg as Record<string, unknown>)[k] = signed.m[k]
166
+ }
167
+ }
168
+ }
169
+ const hash = await utils.sha256(msg)
170
+ const sig = new Uint8Array(SafeBuffer.from(signed.s, "base64"))
171
+ const alg = { name: "ECDSA", hash: { name: "SHA-256" } }
172
+ if (await utils.subtle.verify(alg, key, sig, new Uint8Array(hash))) {
173
+ const verified = utils.parse(signed.m as string)
174
+ if (cb) cb(verified)
175
+ return verified
176
+ }
177
+
178
+ if (cb) cb(null)
179
+ return null
180
+ },
181
+
182
+ /**
183
+ * Sign data with a key pair
184
+ */
185
+ sign: async (
186
+ data: string | Record<string, unknown>,
187
+ pair: Partial<UserPair> | null,
188
+ cb?: Callback<SignedData | null>
189
+ ): Promise<SignedData | null> => {
190
+ if (!pair || !pair.pub || !pair.priv) {
191
+ if (cb) cb(null)
192
+ return null
193
+ }
194
+
195
+ let msg: string | Record<string, unknown> = {}
196
+ if (typeof data === "string") {
197
+ msg = data
198
+ } else {
199
+ const check = utils.parse(data as never) as Record<string, unknown>
200
+ for (const k of Object.keys(check).sort()) {
201
+ if (k !== "_" && k !== userPublicKey && k !== userSignature) {
202
+ (msg as Record<string, unknown>)[k] = check[k]
203
+ }
204
+ }
205
+ }
206
+ const hash = await utils.sha256(msg)
207
+ const jwk = utils.jwk(pair.pub, pair.priv)
208
+ const alg = { name: "ECDSA", namedCurve: "P-256" }
209
+ const sig = await utils.subtle
210
+ .importKey("jwk", jwk, alg, false, ["sign"])
211
+ .then(key =>
212
+ utils.subtle.sign(
213
+ { name: "ECDSA", hash: { name: "SHA-256" } },
214
+ key,
215
+ new Uint8Array(hash)
216
+ )
217
+ )
218
+ const signed: SignedData = {
219
+ m: msg,
220
+ s: SafeBuffer.from(sig, "binary").toString("base64"),
221
+ }
222
+
223
+ if (cb) cb(signed)
224
+ return signed
225
+ },
226
+
227
+ /**
228
+ * Sign a timestamp to prove ownership of an update
229
+ */
230
+ signTimestamp: async (
231
+ timestamp: number,
232
+ pair: Partial<UserPair> | null,
233
+ cb?: Callback<string | null>
234
+ ): Promise<string | null> => {
235
+ if (!pair || !pair.pub || !pair.priv) {
236
+ if (cb) cb(null)
237
+ return null
238
+ }
239
+
240
+ // Convert timestamp to string for consistent hashing
241
+ const timestampStr = timestamp.toString()
242
+ const hash = await utils.sha256(timestampStr)
243
+ const jwk = utils.jwk(pair.pub, pair.priv)
244
+ const alg = { name: "ECDSA", namedCurve: "P-256" }
245
+
246
+ const sig = await utils.subtle
247
+ .importKey("jwk", jwk, alg, false, ["sign"])
248
+ .then(key =>
249
+ utils.subtle.sign(
250
+ { name: "ECDSA", hash: { name: "SHA-256" } },
251
+ key,
252
+ new Uint8Array(hash)
253
+ )
254
+ )
255
+
256
+ const signature = SafeBuffer.from(sig, "binary").toString("base64")
257
+
258
+ if (cb) cb(signature)
259
+ return signature
260
+ },
261
+
262
+ /**
263
+ * Verify a timestamp signature to confirm ownership of update
264
+ */
265
+ verifyTimestamp: async (
266
+ timestamp: number,
267
+ signature: string,
268
+ publicKey: string,
269
+ cb?: Callback<boolean>
270
+ ): Promise<boolean> => {
271
+ if (!publicKey || !timestamp || !signature) {
272
+ if (cb) cb(false)
273
+ return false
274
+ }
275
+
276
+ try {
277
+ // Convert timestamp to string for consistent hashing
278
+ const timestampStr = timestamp.toString()
279
+ const hash = await utils.sha256(timestampStr)
280
+
281
+ const key = await utils.subtle.importKey(
282
+ "jwk",
283
+ utils.jwk(publicKey),
284
+ { name: "ECDSA", namedCurve: "P-256" },
285
+ false,
286
+ ["verify"]
287
+ )
288
+
289
+ const sig = new Uint8Array(SafeBuffer.from(signature, "base64"))
290
+ const alg = { name: "ECDSA", hash: { name: "SHA-256" } }
291
+
292
+ const isValid = await utils.subtle.verify(
293
+ alg,
294
+ key,
295
+ sig,
296
+ new Uint8Array(hash)
297
+ )
298
+
299
+ if (cb) cb(isValid)
300
+ return isValid
301
+ } catch (err) {
302
+ console.log(`error verifying timestamp signature: ${(err as Error).message}`)
303
+ if (cb) cb(false)
304
+ return false
305
+ }
306
+ },
307
+
308
+ /**
309
+ * Key derivation using PBKDF2
310
+ */
311
+ work: async (
312
+ data: unknown,
313
+ salt?: unknown,
314
+ cb?: Callback<{ epriv: string }>
315
+ ): Promise<{ epriv: string }> => {
316
+ let _cb = cb
317
+ let _salt = salt
318
+
319
+ if (typeof salt === "function") {
320
+ _cb = salt as Callback<{ epriv: string }>
321
+ _salt = undefined
322
+ }
323
+ if (typeof _salt === "undefined") _salt = utils.random(9)
324
+
325
+ const key = await utils.subtle.importKey(
326
+ "raw",
327
+ new TextEncoder().encode(utils.stringify(data)),
328
+ { name: "PBKDF2" },
329
+ false,
330
+ ["deriveBits"]
331
+ )
332
+ const alg = {
333
+ name: "PBKDF2",
334
+ iterations: 100000,
335
+ salt: new TextEncoder().encode(_salt as string),
336
+ hash: { name: "SHA-256" },
337
+ }
338
+ const work = await utils.subtle.deriveBits(alg, key, 512)
339
+ // Use "pair" format so that work can be used as epriv by decrypt
340
+ const pair = { epriv: SafeBuffer.from(work, "binary").toString("base64") }
341
+ if (_cb) _cb(pair)
342
+ return pair
343
+ },
344
+
345
+ /**
346
+ * Derive shared secret using ECDH
347
+ */
348
+ secret: async (
349
+ to: Partial<UserPair> | null,
350
+ from: Partial<UserPair> | null,
351
+ cb?: Callback<{ epriv: string } | null>
352
+ ): Promise<{ epriv: string } | null> => {
353
+ if (!to || !to.epub || !from || !from.epub || !from.epriv) {
354
+ if (cb) cb(null)
355
+ return null
356
+ }
357
+
358
+ const alg = { name: "ECDH", namedCurve: "P-256" }
359
+ const pub = utils.jwk(to.epub)
360
+ const pubKey = await utils.subtle.importKey("jwk", pub, alg, true, [])
361
+ const priv = utils.jwk(from.epub, from.epriv)
362
+ // utils.jwk provides default key_ops but it shouldn't be used here
363
+ delete (priv as { key_ops?: unknown }).key_ops
364
+ const derived = await utils.subtle
365
+ .importKey("jwk", priv, alg, false, ["deriveBits"])
366
+ .then(async key => {
367
+ const derivedBits = await utils.subtle.deriveBits(
368
+ { public: pubKey, name: "ECDH", namedCurve: "P-256" } as never,
369
+ key,
370
+ 256
371
+ )
372
+ const derivedKey = await utils.subtle.importKey(
373
+ "raw",
374
+ new Uint8Array(derivedBits),
375
+ { name: "AES-GCM", length: 256 },
376
+ true,
377
+ ["encrypt", "decrypt"]
378
+ )
379
+ return utils.subtle.exportKey("jwk", derivedKey).then(({ k }) => k!)
380
+ })
381
+ // Use "pair" format so that secret can be used as epriv by encrypt
382
+ if (cb) cb({ epriv: derived })
383
+ return { epriv: derived }
384
+ },
385
+ }
386
+
387
+ export default SEA
388
+