@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.
- package/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/dist/holster.js +4 -0
- package/dist/holster.js.map +7 -0
- package/package.json +23 -5
- package/src/get.js +11 -19
- package/src/ham.js +17 -47
- package/src/holster.js +106 -28
- package/src/sea.js +0 -52
- package/src/store.js +7 -7
- package/src/wire.js +8 -2
- package/ts/src/array.ts +78 -0
- package/ts/src/buffer.ts +441 -0
- package/ts/src/dup.ts +49 -0
- package/ts/src/get.ts +83 -0
- package/ts/src/ham.ts +254 -0
- package/ts/src/holster.ts +931 -0
- package/ts/src/index.ts +35 -0
- package/ts/src/radisk.ts +585 -0
- package/ts/src/radix.ts +157 -0
- package/ts/src/schemas.ts +519 -0
- package/ts/src/sea-utils.ts +103 -0
- package/ts/src/sea.ts +388 -0
- package/ts/src/store.ts +321 -0
- package/ts/src/user.ts +381 -0
- package/ts/src/utils.ts +232 -0
- package/ts/src/wire.ts +829 -0
- package/.dockerignore +0 -9
- package/.github/workflows/node.js.yml +0 -31
- package/.github/workflows/npm-publish.yml +0 -33
- package/Dockerfile +0 -20
- package/examples/index.html +0 -33
- package/examples/index.js +0 -49
- package/examples/user.html +0 -31
- package/examples/user.js +0 -93
- package/examples/wire.html +0 -27
- package/examples/wire.js +0 -55
- package/test/holster.get.test.js +0 -94
- package/test/holster.lex.test.js +0 -196
- package/test/holster.off.test.js +0 -53
- package/test/holster.on.test.js +0 -264
- package/test/holster.put.test.js +0 -358
- package/test/holster.secure.test.js +0 -202
- package/test/holster.user.get.test.js +0 -114
- package/test/holster.user.lex.test.js +0 -149
- package/test/holster.user.off.test.js +0 -73
- package/test/holster.user.on.test.js +0 -300
- package/test/holster.user.put.test.js +0 -463
- package/test/kitty/! +0 -9
- package/test/names.js +0 -225
- package/test/radisk.cache.test.js +0 -213
- package/test/radisk.test.js +0 -331
- package/test/radix.test.js +0 -117
- package/test/sea.test.js +0 -284
- package/test/split.test.js +0 -317
- package/test/store.test.js +0 -316
- package/test/system/basic.test.js +0 -29
- package/test/system/chained-on.test.js +0 -41
- package/test/system/concurrent-listeners.test.js +0 -125
- package/test/system/immediate-read.test.js +0 -30
- package/test/system/listener-before-data.test.js +0 -51
- package/test/system/promise-wrapper.test.js +0 -47
- package/test/system/user-chained-on.test.js +0 -60
- package/test/system/user-concurrent-listeners.test.js +0 -142
- package/test/system/user-listener-before-data.test.js +0 -66
- package/test/user.test.js +0 -173
- package/test/wire.test.js +0 -158
package/ts/src/buffer.ts
ADDED
|
@@ -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
|
+
|