@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,441 @@
1
+ /**
2
+ * SafeBuffer - Secure Buffer implementation with validation
3
+ * Compatible with Node.js Buffer API but with enhanced security checks
4
+ */
5
+
6
+ import SeaArray from "./array.ts"
7
+
8
+ type Encoding = "utf8" | "utf-8" | "hex" | "base64" | "binary" | "latin1" | "ascii"
9
+
10
+ // Security constants
11
+ const SECURITY_LIMITS = {
12
+ MAX_BUFFER_SIZE: 10 * 1024 * 1024, // 10MB limit
13
+ MAX_STRING_LENGTH: 1024 * 1024, // 1MB string limit
14
+ }
15
+
16
+ // Type definition for SafeBuffer static interface
17
+ interface SafeBufferConstructor {
18
+ (...props: unknown[]): SeaArray
19
+ prototype: typeof Array.prototype
20
+ from(input: string, encoding?: Encoding): SeaArray
21
+ from(input: ArrayBuffer | ArrayBufferView | number[]): SeaArray
22
+ from(...args: unknown[]): SeaArray
23
+ alloc(length: number, fill?: number | string): SeaArray
24
+ concat(arr: ArrayLike<unknown>[]): SeaArray
25
+ isBuffer(obj: unknown): obj is SeaArray
26
+ byteLength(string: string, encoding?: Encoding): number
27
+ }
28
+
29
+ /**
30
+ * SafeBuffer constructor (deprecated - use SafeBuffer.from())
31
+ */
32
+ const SafeBuffer = function SafeBuffer(..._props: unknown[]): SeaArray {
33
+ console.warn("new SafeBuffer() is deprecated, please use SafeBuffer.from()")
34
+ // Will be implemented after from() is defined
35
+ return SeaArray.from([])
36
+ } as SafeBufferConstructor
37
+
38
+ SafeBuffer.prototype = Object.create(Array.prototype)
39
+
40
+ // ============================================================================
41
+ // Input Validation Helpers
42
+ // ============================================================================
43
+
44
+ const validateStringInput = (input: string, _encoding: Encoding): void => {
45
+ if (input.length > SECURITY_LIMITS.MAX_STRING_LENGTH) {
46
+ throw new RangeError(
47
+ `String too large: ${input.length} > ${SECURITY_LIMITS.MAX_STRING_LENGTH}`
48
+ )
49
+ }
50
+ }
51
+
52
+ const validateBufferSize = (size: number, operation = "buffer operation"): void => {
53
+ if (size > SECURITY_LIMITS.MAX_BUFFER_SIZE) {
54
+ throw new RangeError(
55
+ `Buffer size too large for ${operation}: ${size} > ${SECURITY_LIMITS.MAX_BUFFER_SIZE}`
56
+ )
57
+ }
58
+ }
59
+
60
+ // ============================================================================
61
+ // Encoding Parsers
62
+ // ============================================================================
63
+
64
+ const parseHexString = (hexString: string): Uint8Array => {
65
+ validateStringInput(hexString, "hex")
66
+
67
+ const cleanHex = hexString.replace(/\s/g, "").toLowerCase()
68
+
69
+ if (!/^[0-9a-f]*$/.test(cleanHex)) {
70
+ throw new TypeError("Invalid hex string: contains non-hex characters")
71
+ }
72
+
73
+ if (cleanHex.length % 2 !== 0) {
74
+ throw new TypeError("Invalid hex string: must have even length")
75
+ }
76
+
77
+ if (cleanHex.length === 0) {
78
+ return new Uint8Array(0)
79
+ }
80
+
81
+ validateBufferSize(cleanHex.length / 2, "hex parsing")
82
+
83
+ const bytes = new Uint8Array(cleanHex.length / 2)
84
+ for (let i = 0; i < cleanHex.length; i += 2) {
85
+ const hexByte = cleanHex.substr(i, 2)
86
+ bytes[i / 2] = parseInt(hexByte, 16)
87
+ }
88
+ return bytes
89
+ }
90
+
91
+ const encodeUtf8String = (str: string): Uint8Array => {
92
+ validateStringInput(str, "utf8")
93
+
94
+ try {
95
+ const encoder = new TextEncoder()
96
+ const bytes = encoder.encode(str)
97
+ validateBufferSize(bytes.length, "UTF-8 encoding")
98
+ return bytes
99
+ } catch (err) {
100
+ throw new TypeError(`Failed to encode UTF-8 string: ${(err as Error).message}`)
101
+ }
102
+ }
103
+
104
+ const encodeBinaryString = (str: string): Uint8Array => {
105
+ validateStringInput(str, "binary")
106
+ validateBufferSize(str.length, "binary encoding")
107
+
108
+ const bytes = new Uint8Array(str.length)
109
+ for (let i = 0; i < str.length; i++) {
110
+ const charCode = str.charCodeAt(i)
111
+ if (charCode > 255) {
112
+ throw new TypeError(
113
+ `Invalid binary string: character at position ${i} exceeds byte range (${charCode} > 255)`
114
+ )
115
+ }
116
+ bytes[i] = charCode
117
+ }
118
+ return bytes
119
+ }
120
+
121
+ const parseBase64String = (base64String: string): Uint8Array => {
122
+ validateStringInput(base64String, "base64")
123
+
124
+ const cleanBase64 = base64String.replace(/\s/g, "")
125
+
126
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanBase64)) {
127
+ throw new TypeError("Invalid base64 string: contains invalid characters")
128
+ }
129
+
130
+ try {
131
+ const decodedString = atob(cleanBase64)
132
+ validateBufferSize(decodedString.length, "base64 decoding")
133
+
134
+ const bytes = new Uint8Array(decodedString.length)
135
+ for (let i = 0; i < decodedString.length; i++) {
136
+ bytes[i] = decodedString.charCodeAt(i)
137
+ }
138
+ return bytes
139
+ } catch (err) {
140
+ throw new TypeError(`Failed to decode base64 string: ${(err as Error).message}`)
141
+ }
142
+ }
143
+
144
+ const processArrayLikeInput = (input: unknown): Uint8Array => {
145
+ if (input instanceof ArrayBuffer) {
146
+ validateBufferSize(input.byteLength, "ArrayBuffer processing")
147
+ return new Uint8Array(input)
148
+ }
149
+
150
+ if (ArrayBuffer.isView(input)) {
151
+ validateBufferSize(input.byteLength, "TypedArray processing")
152
+ return new Uint8Array(input.buffer, input.byteOffset, input.byteLength)
153
+ }
154
+
155
+ if (Array.isArray(input)) {
156
+ validateBufferSize(input.length, "Array processing")
157
+
158
+ for (let i = 0; i < input.length; i++) {
159
+ const val = input[i]
160
+ if (
161
+ typeof val !== "number" ||
162
+ val < 0 ||
163
+ val > 255 ||
164
+ !Number.isInteger(val)
165
+ ) {
166
+ throw new TypeError(
167
+ `Invalid byte value at index ${i}: ${val} (must be integer 0-255)`
168
+ )
169
+ }
170
+ }
171
+ return new Uint8Array(input)
172
+ }
173
+
174
+ if (
175
+ typeof input === "object" &&
176
+ input !== null &&
177
+ "byteLength" in input &&
178
+ typeof (input as { byteLength: unknown }).byteLength === "number"
179
+ ) {
180
+ validateBufferSize((input as { byteLength: number }).byteLength, "ArrayBuffer-like processing")
181
+ return new Uint8Array(input as ArrayBuffer)
182
+ }
183
+
184
+ if (
185
+ typeof input === "object" &&
186
+ input !== null &&
187
+ "length" in input &&
188
+ typeof (input as { length: unknown }).length === "number"
189
+ ) {
190
+ const arrayLike = input as ArrayLike<unknown>
191
+ validateBufferSize(arrayLike.length, "array-like processing")
192
+
193
+ const arr = Array.from(arrayLike)
194
+ for (let i = 0; i < arr.length; i++) {
195
+ const val = arr[i]
196
+ if (
197
+ typeof val !== "number" ||
198
+ val < 0 ||
199
+ val > 255 ||
200
+ !Number.isInteger(val)
201
+ ) {
202
+ throw new TypeError(
203
+ `Invalid byte value at index ${i}: ${val} (must be integer 0-255)`
204
+ )
205
+ }
206
+ }
207
+ return new Uint8Array(arr as number[])
208
+ }
209
+
210
+ throw new TypeError("Unsupported input type: " + typeof input)
211
+ }
212
+
213
+ // ============================================================================
214
+ // SafeBuffer Static Methods
215
+ // ============================================================================
216
+
217
+ Object.assign(SafeBuffer, {
218
+ from(...args: unknown[]): SeaArray {
219
+ const argCount = args.length
220
+
221
+ if (argCount === 0) {
222
+ throw new TypeError("SafeBuffer.from() requires at least one argument")
223
+ }
224
+
225
+ const input = args[0]
226
+
227
+ if (input === null || input === undefined) {
228
+ throw new TypeError("First argument must not be null or undefined")
229
+ }
230
+
231
+ let bytes: Uint8Array
232
+
233
+ if (typeof input === "string") {
234
+ const encoding = ((args[1] as string) || "utf8").toLowerCase() as Encoding
235
+
236
+ switch (encoding) {
237
+ case "hex":
238
+ bytes = parseHexString(input)
239
+ break
240
+
241
+ case "utf8":
242
+ case "utf-8":
243
+ bytes = encodeUtf8String(input)
244
+ break
245
+
246
+ case "binary":
247
+ case "latin1":
248
+ bytes = encodeBinaryString(input)
249
+ break
250
+
251
+ case "base64":
252
+ bytes = parseBase64String(input)
253
+ break
254
+
255
+ case "ascii":
256
+ validateStringInput(input, "ascii")
257
+ for (let i = 0; i < input.length; i++) {
258
+ if (input.charCodeAt(i) > 127) {
259
+ throw new TypeError(
260
+ `Invalid ASCII character at position ${i}: ${input.charCodeAt(i)} > 127`
261
+ )
262
+ }
263
+ }
264
+ bytes = encodeUtf8String(input)
265
+ break
266
+
267
+ default:
268
+ throw new TypeError("Unknown encoding: " + encoding)
269
+ }
270
+ } else {
271
+ bytes = processArrayLikeInput(input)
272
+ }
273
+
274
+ if (!bytes || bytes.length === 0) {
275
+ return SeaArray.from(new Uint8Array(0))
276
+ }
277
+
278
+ try {
279
+ return SeaArray.from(bytes)
280
+ } catch (err) {
281
+ throw new Error(`Failed to create SafeBuffer: ${(err as Error).message}`)
282
+ }
283
+ },
284
+
285
+ alloc(length: number, fill: number | string = 0): SeaArray {
286
+ if (typeof length !== "number" || !Number.isInteger(length) || length < 0) {
287
+ throw new TypeError("Length must be a non-negative integer")
288
+ }
289
+
290
+ validateBufferSize(length, "buffer allocation")
291
+
292
+ let fillValue: number
293
+ if (typeof fill === "number") {
294
+ if (fill < 0 || fill > 255 || !Number.isInteger(fill)) {
295
+ throw new TypeError("Fill value must be an integer between 0 and 255")
296
+ }
297
+ fillValue = fill
298
+ } else if (typeof fill === "string") {
299
+ if (fill.length !== 1) {
300
+ throw new TypeError("Fill string must be exactly one character")
301
+ }
302
+ fillValue = fill.charCodeAt(0)
303
+ if (fillValue > 255) {
304
+ throw new TypeError("Fill character code must be <= 255")
305
+ }
306
+ } else {
307
+ throw new TypeError("Fill must be a number or single character string")
308
+ }
309
+
310
+ try {
311
+ const buffer = new Uint8Array(length)
312
+ if (fillValue !== 0) {
313
+ buffer.fill(fillValue)
314
+ }
315
+ return SeaArray.from(buffer)
316
+ } catch (err) {
317
+ throw new Error(`Failed to allocate buffer: ${(err as Error).message}`)
318
+ }
319
+ },
320
+
321
+ concat(arr: ArrayLike<unknown>[]): SeaArray {
322
+ if (!Array.isArray(arr)) {
323
+ throw new TypeError("First argument must be an array")
324
+ }
325
+
326
+ if (arr.length === 0) {
327
+ return SeaArray.from(new Uint8Array(0))
328
+ }
329
+
330
+ let totalLength = 0
331
+ const validatedBuffers: Uint8Array[] = []
332
+
333
+ for (let i = 0; i < arr.length; i++) {
334
+ const item = arr[i]
335
+
336
+ if (!item) {
337
+ throw new TypeError(`Array element at index ${i} is null or undefined`)
338
+ }
339
+
340
+ let bytes: Uint8Array
341
+ try {
342
+ bytes = processArrayLikeInput(item)
343
+ } catch (err) {
344
+ throw new TypeError(
345
+ `Invalid array element at index ${i}: ${(err as Error).message}`
346
+ )
347
+ }
348
+
349
+ totalLength += bytes.length
350
+ validateBufferSize(totalLength, "buffer concatenation")
351
+ validatedBuffers.push(bytes)
352
+ }
353
+
354
+ try {
355
+ const result = new Uint8Array(totalLength)
356
+ let offset = 0
357
+
358
+ for (const buffer of validatedBuffers) {
359
+ result.set(buffer, offset)
360
+ offset += buffer.length
361
+ }
362
+
363
+ return SeaArray.from(result)
364
+ } catch (err) {
365
+ throw new Error(`Failed to concatenate buffers: ${(err as Error).message}`)
366
+ }
367
+ },
368
+
369
+ isBuffer(obj: unknown): obj is SeaArray {
370
+ return (
371
+ obj instanceof SeaArray ||
372
+ (obj !== null &&
373
+ typeof obj === "object" &&
374
+ (obj as { constructor?: unknown }).constructor === SafeBuffer)
375
+ )
376
+ },
377
+
378
+ byteLength(string: string, encoding: Encoding = "utf8"): number {
379
+ if (typeof string !== "string") {
380
+ throw new TypeError("First argument must be a string")
381
+ }
382
+
383
+ switch (encoding.toLowerCase()) {
384
+ case "utf8":
385
+ case "utf-8":
386
+ return new TextEncoder().encode(string).length
387
+
388
+ case "ascii":
389
+ case "binary":
390
+ case "latin1":
391
+ return string.length
392
+
393
+ case "base64": {
394
+ const clean = string.replace(/\s/g, "")
395
+ return (
396
+ Math.floor((clean.length * 3) / 4) - (clean.match(/=/g) || []).length
397
+ )
398
+ }
399
+
400
+ case "hex":
401
+ return Math.floor(string.replace(/\s/g, "").length / 2)
402
+
403
+ default:
404
+ throw new TypeError("Unknown encoding: " + encoding)
405
+ }
406
+ },
407
+ })
408
+
409
+ // Add instance method for toString
410
+ SafeBuffer.prototype.toString = function (
411
+ this: SeaArray,
412
+ encoding: Encoding = "utf8",
413
+ start = 0,
414
+ end = this.length
415
+ ): string {
416
+ if (typeof encoding !== "string") {
417
+ throw new TypeError("Encoding must be a string")
418
+ }
419
+
420
+ if (typeof start !== "number" || !Number.isInteger(start) || start < 0) {
421
+ throw new TypeError("Start must be a non-negative integer")
422
+ }
423
+
424
+ if (typeof end !== "number" || !Number.isInteger(end) || end < start) {
425
+ throw new TypeError("End must be an integer >= start")
426
+ }
427
+
428
+ try {
429
+ return SeaArray.prototype.toString.call(
430
+ this,
431
+ encoding as "utf8" | "hex" | "base64",
432
+ start,
433
+ end
434
+ )
435
+ } catch (err) {
436
+ throw new Error(`Failed to convert buffer to string: ${(err as Error).message}`)
437
+ }
438
+ }
439
+
440
+ export default SafeBuffer
441
+
package/ts/src/dup.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Dup - Duplicate message tracking for wire protocol
3
+ * Prevents processing the same message multiple times
4
+ */
5
+
6
+ export interface DupInterface {
7
+ store: Record<string, number>
8
+ check: (id: string) => string | false
9
+ track: (id: string) => string
10
+ expiry: NodeJS.Timeout | null
11
+ }
12
+
13
+ /**
14
+ * Create a duplicate message tracker
15
+ * @param maxAge - Maximum age of tracked messages in milliseconds (default: 9000)
16
+ * @returns Dup interface for checking and tracking message IDs
17
+ */
18
+ const Dup = (maxAge?: number): DupInterface => {
19
+ const maxAgeMs = maxAge || 9000
20
+ const dup: DupInterface = {
21
+ store: {},
22
+ expiry: null,
23
+ check: (id: string): string | false => {
24
+ return dup.store[id] ? dup.track(id) : false
25
+ },
26
+ track: (id: string): string => {
27
+ // Keep the liveliness of the message up while it is being received
28
+ dup.store[id] = Date.now()
29
+ if (!dup.expiry) {
30
+ dup.expiry = setTimeout(() => {
31
+ if (!dup.expiry) return
32
+
33
+ const now = Date.now()
34
+ Object.keys(dup.store).forEach(id => {
35
+ if (now - dup.store[id]! > maxAgeMs) {
36
+ delete dup.store[id]
37
+ }
38
+ })
39
+ dup.expiry = null
40
+ }, maxAgeMs)
41
+ }
42
+ return id
43
+ },
44
+ }
45
+ return dup
46
+ }
47
+
48
+ export default Dup
49
+
package/ts/src/get.ts ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Get - Query data from the graph
3
+ * Extracts nodes and properties based on lex queries
4
+ */
5
+
6
+ import { match, userPublicKey } from "./utils.ts"
7
+ import type { Lex, Graph } from "./schemas.ts"
8
+ import type { GraphNode } from "./schemas.ts"
9
+
10
+ /**
11
+ * Get data from graph based on lex query
12
+ * @param lex - Query specification with soul and optional property filter
13
+ * @param graph - In-memory graph
14
+ * @param fast - If true, return current data; if false, return undefined to trigger store lookup
15
+ */
16
+ const Get = (lex: Lex, graph: Graph, fast?: boolean): Graph | undefined => {
17
+ if (!lex || typeof lex !== "object") {
18
+ throw new TypeError("lex must be an object")
19
+ }
20
+ if (!graph || typeof graph !== "object") {
21
+ throw new TypeError("graph must be an object")
22
+ }
23
+
24
+ const soul = lex["#"]
25
+ if (!soul || typeof soul !== "string") {
26
+ throw new TypeError("soul must be a string")
27
+ }
28
+
29
+ if (!graph[soul]) return undefined
30
+
31
+ const stateVector: Record<string, number> = {}
32
+ const node: GraphNode = { _: { "#": soul, ">": stateVector } }
33
+ const signatures: Record<string, string> = {}
34
+
35
+ if (typeof lex["."] === "string") {
36
+ const key = lex["."]
37
+ if (typeof graph[soul]![key] === "undefined") return undefined
38
+
39
+ node[key] = graph[soul]![key]
40
+ node._[">"][key] = graph[soul]!._[">"][key]!
41
+ const state = graph[soul]!._[">"][key]!
42
+ const sigs = graph[soul]!._["s"]
43
+ if (sigs && sigs[state]) {
44
+ signatures[state.toString()] = sigs[state]!
45
+ }
46
+
47
+ // Always include userPublicKey for verification, regardless of filter
48
+ if (userPublicKey && typeof graph[soul]![userPublicKey] !== "undefined") {
49
+ node[userPublicKey] = graph[soul]![userPublicKey]
50
+ node._[">"][userPublicKey] = graph[soul]!._[">"][userPublicKey]!
51
+ const pkState = graph[soul]!._[">"][userPublicKey]!
52
+ const pkSigs = graph[soul]!._["s"]
53
+ if (pkSigs && pkSigs[pkState]) {
54
+ signatures[pkState.toString()] = pkSigs[pkState]!
55
+ }
56
+ }
57
+ } else {
58
+ // For fast requests, return properties currently in the graph. Otherwise
59
+ // return undefined to ensure we get all matching properties from store.
60
+ if (!fast) return undefined
61
+
62
+ for (const key of Object.keys(graph[soul]!)) {
63
+ // Always include userPublicKey for verification, regardless of filter
64
+ if (key === userPublicKey || match(lex["."], key)) {
65
+ node[key] = graph[soul]![key]!
66
+ node._[">"][key] = graph[soul]!._[">"][key]!
67
+ const state = graph[soul]!._[">"][key]!
68
+ const sigs = graph[soul]!._["s"]
69
+ if (sigs && sigs[state]) {
70
+ signatures[state.toString()] = sigs[state]!
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ if (Object.keys(signatures).length > 0) {
77
+ node._["s"] = signatures
78
+ }
79
+ return { [soul]: node }
80
+ }
81
+
82
+ export default Get
83
+