@mblaney/holster 2.0.1 → 2.1.0
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 +37 -6
- package/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/dist/holster.js +3 -3
- package/dist/holster.js.map +3 -3
- package/package.json +1 -1
- package/src/ham.js +10 -3
- package/src/holster.js +36 -25
- package/src/radisk.js +26 -28
- package/src/store.js +30 -17
- package/src/user.js +92 -101
- package/src/wire.js +325 -125
- package/ts/src/array.ts +10 -13
- package/ts/src/buffer.ts +40 -20
- package/ts/src/get.ts +5 -6
- package/ts/src/ham.ts +47 -28
- package/ts/src/holster.ts +277 -132
- package/ts/src/index.ts +14 -15
- package/ts/src/radisk.ts +122 -51
- package/ts/src/radix.ts +50 -34
- package/ts/src/schemas.ts +44 -24
- package/ts/src/sea-utils.ts +7 -11
- package/ts/src/sea.ts +51 -54
- package/ts/src/store.ts +76 -58
- package/ts/src/user.ts +136 -113
- package/ts/src/utils.ts +36 -11
- package/ts/src/wire.ts +477 -248
package/ts/src/buffer.ts
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
import SeaArray from "./array.ts"
|
|
7
7
|
|
|
8
|
-
type Encoding =
|
|
8
|
+
type Encoding =
|
|
9
|
+
| "utf8"
|
|
10
|
+
| "utf-8"
|
|
11
|
+
| "hex"
|
|
12
|
+
| "base64"
|
|
13
|
+
| "binary"
|
|
14
|
+
| "latin1"
|
|
15
|
+
| "ascii"
|
|
9
16
|
|
|
10
17
|
// Security constants
|
|
11
18
|
const SECURITY_LIMITS = {
|
|
@@ -44,15 +51,18 @@ SafeBuffer.prototype = Object.create(Array.prototype)
|
|
|
44
51
|
const validateStringInput = (input: string, _encoding: Encoding): void => {
|
|
45
52
|
if (input.length > SECURITY_LIMITS.MAX_STRING_LENGTH) {
|
|
46
53
|
throw new RangeError(
|
|
47
|
-
`String too large: ${input.length} > ${SECURITY_LIMITS.MAX_STRING_LENGTH}
|
|
54
|
+
`String too large: ${input.length} > ${SECURITY_LIMITS.MAX_STRING_LENGTH}`,
|
|
48
55
|
)
|
|
49
56
|
}
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
const validateBufferSize = (
|
|
59
|
+
const validateBufferSize = (
|
|
60
|
+
size: number,
|
|
61
|
+
operation = "buffer operation",
|
|
62
|
+
): void => {
|
|
53
63
|
if (size > SECURITY_LIMITS.MAX_BUFFER_SIZE) {
|
|
54
64
|
throw new RangeError(
|
|
55
|
-
`Buffer size too large for ${operation}: ${size} > ${SECURITY_LIMITS.MAX_BUFFER_SIZE}
|
|
65
|
+
`Buffer size too large for ${operation}: ${size} > ${SECURITY_LIMITS.MAX_BUFFER_SIZE}`,
|
|
56
66
|
)
|
|
57
67
|
}
|
|
58
68
|
}
|
|
@@ -97,7 +107,9 @@ const encodeUtf8String = (str: string): Uint8Array => {
|
|
|
97
107
|
validateBufferSize(bytes.length, "UTF-8 encoding")
|
|
98
108
|
return bytes
|
|
99
109
|
} catch (err) {
|
|
100
|
-
throw new TypeError(
|
|
110
|
+
throw new TypeError(
|
|
111
|
+
`Failed to encode UTF-8 string: ${(err as Error).message}`,
|
|
112
|
+
)
|
|
101
113
|
}
|
|
102
114
|
}
|
|
103
115
|
|
|
@@ -110,7 +122,7 @@ const encodeBinaryString = (str: string): Uint8Array => {
|
|
|
110
122
|
const charCode = str.charCodeAt(i)
|
|
111
123
|
if (charCode > 255) {
|
|
112
124
|
throw new TypeError(
|
|
113
|
-
`Invalid binary string: character at position ${i} exceeds byte range (${charCode} > 255)
|
|
125
|
+
`Invalid binary string: character at position ${i} exceeds byte range (${charCode} > 255)`,
|
|
114
126
|
)
|
|
115
127
|
}
|
|
116
128
|
bytes[i] = charCode
|
|
@@ -137,7 +149,9 @@ const parseBase64String = (base64String: string): Uint8Array => {
|
|
|
137
149
|
}
|
|
138
150
|
return bytes
|
|
139
151
|
} catch (err) {
|
|
140
|
-
throw new TypeError(
|
|
152
|
+
throw new TypeError(
|
|
153
|
+
`Failed to decode base64 string: ${(err as Error).message}`,
|
|
154
|
+
)
|
|
141
155
|
}
|
|
142
156
|
}
|
|
143
157
|
|
|
@@ -164,7 +178,7 @@ const processArrayLikeInput = (input: unknown): Uint8Array => {
|
|
|
164
178
|
!Number.isInteger(val)
|
|
165
179
|
) {
|
|
166
180
|
throw new TypeError(
|
|
167
|
-
`Invalid byte value at index ${i}: ${val} (must be integer 0-255)
|
|
181
|
+
`Invalid byte value at index ${i}: ${val} (must be integer 0-255)`,
|
|
168
182
|
)
|
|
169
183
|
}
|
|
170
184
|
}
|
|
@@ -175,9 +189,12 @@ const processArrayLikeInput = (input: unknown): Uint8Array => {
|
|
|
175
189
|
typeof input === "object" &&
|
|
176
190
|
input !== null &&
|
|
177
191
|
"byteLength" in input &&
|
|
178
|
-
typeof (input as {
|
|
192
|
+
typeof (input as {byteLength: unknown}).byteLength === "number"
|
|
179
193
|
) {
|
|
180
|
-
validateBufferSize(
|
|
194
|
+
validateBufferSize(
|
|
195
|
+
(input as {byteLength: number}).byteLength,
|
|
196
|
+
"ArrayBuffer-like processing",
|
|
197
|
+
)
|
|
181
198
|
return new Uint8Array(input as ArrayBuffer)
|
|
182
199
|
}
|
|
183
200
|
|
|
@@ -185,7 +202,7 @@ const processArrayLikeInput = (input: unknown): Uint8Array => {
|
|
|
185
202
|
typeof input === "object" &&
|
|
186
203
|
input !== null &&
|
|
187
204
|
"length" in input &&
|
|
188
|
-
typeof (input as {
|
|
205
|
+
typeof (input as {length: unknown}).length === "number"
|
|
189
206
|
) {
|
|
190
207
|
const arrayLike = input as ArrayLike<unknown>
|
|
191
208
|
validateBufferSize(arrayLike.length, "array-like processing")
|
|
@@ -200,7 +217,7 @@ const processArrayLikeInput = (input: unknown): Uint8Array => {
|
|
|
200
217
|
!Number.isInteger(val)
|
|
201
218
|
) {
|
|
202
219
|
throw new TypeError(
|
|
203
|
-
`Invalid byte value at index ${i}: ${val} (must be integer 0-255)
|
|
220
|
+
`Invalid byte value at index ${i}: ${val} (must be integer 0-255)`,
|
|
204
221
|
)
|
|
205
222
|
}
|
|
206
223
|
}
|
|
@@ -257,7 +274,7 @@ Object.assign(SafeBuffer, {
|
|
|
257
274
|
for (let i = 0; i < input.length; i++) {
|
|
258
275
|
if (input.charCodeAt(i) > 127) {
|
|
259
276
|
throw new TypeError(
|
|
260
|
-
`Invalid ASCII character at position ${i}: ${input.charCodeAt(i)} > 127
|
|
277
|
+
`Invalid ASCII character at position ${i}: ${input.charCodeAt(i)} > 127`,
|
|
261
278
|
)
|
|
262
279
|
}
|
|
263
280
|
}
|
|
@@ -342,7 +359,7 @@ Object.assign(SafeBuffer, {
|
|
|
342
359
|
bytes = processArrayLikeInput(item)
|
|
343
360
|
} catch (err) {
|
|
344
361
|
throw new TypeError(
|
|
345
|
-
`Invalid array element at index ${i}: ${(err as Error).message}
|
|
362
|
+
`Invalid array element at index ${i}: ${(err as Error).message}`,
|
|
346
363
|
)
|
|
347
364
|
}
|
|
348
365
|
|
|
@@ -362,7 +379,9 @@ Object.assign(SafeBuffer, {
|
|
|
362
379
|
|
|
363
380
|
return SeaArray.from(result)
|
|
364
381
|
} catch (err) {
|
|
365
|
-
throw new Error(
|
|
382
|
+
throw new Error(
|
|
383
|
+
`Failed to concatenate buffers: ${(err as Error).message}`,
|
|
384
|
+
)
|
|
366
385
|
}
|
|
367
386
|
},
|
|
368
387
|
|
|
@@ -371,7 +390,7 @@ Object.assign(SafeBuffer, {
|
|
|
371
390
|
obj instanceof SeaArray ||
|
|
372
391
|
(obj !== null &&
|
|
373
392
|
typeof obj === "object" &&
|
|
374
|
-
(obj as {
|
|
393
|
+
(obj as {constructor?: unknown}).constructor === SafeBuffer)
|
|
375
394
|
)
|
|
376
395
|
},
|
|
377
396
|
|
|
@@ -411,7 +430,7 @@ SafeBuffer.prototype.toString = function (
|
|
|
411
430
|
this: SeaArray,
|
|
412
431
|
encoding: Encoding = "utf8",
|
|
413
432
|
start = 0,
|
|
414
|
-
end = this.length
|
|
433
|
+
end = this.length,
|
|
415
434
|
): string {
|
|
416
435
|
if (typeof encoding !== "string") {
|
|
417
436
|
throw new TypeError("Encoding must be a string")
|
|
@@ -430,12 +449,13 @@ SafeBuffer.prototype.toString = function (
|
|
|
430
449
|
this,
|
|
431
450
|
encoding as "utf8" | "hex" | "base64",
|
|
432
451
|
start,
|
|
433
|
-
end
|
|
452
|
+
end,
|
|
434
453
|
)
|
|
435
454
|
} catch (err) {
|
|
436
|
-
throw new Error(
|
|
455
|
+
throw new Error(
|
|
456
|
+
`Failed to convert buffer to string: ${(err as Error).message}`,
|
|
457
|
+
)
|
|
437
458
|
}
|
|
438
459
|
}
|
|
439
460
|
|
|
440
461
|
export default SafeBuffer
|
|
441
|
-
|
package/ts/src/get.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Extracts nodes and properties based on lex queries
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
8
|
-
import type {
|
|
6
|
+
import {match, userPublicKey} from "./utils.ts"
|
|
7
|
+
import type {Lex, Graph} from "./schemas.ts"
|
|
8
|
+
import type {GraphNode} from "./schemas.ts"
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Get data from graph based on lex query
|
|
@@ -29,7 +29,7 @@ const Get = (lex: Lex, graph: Graph, fast?: boolean): Graph | undefined => {
|
|
|
29
29
|
if (!graph[soul]) return undefined
|
|
30
30
|
|
|
31
31
|
const stateVector: Record<string, number> = {}
|
|
32
|
-
const node: GraphNode = {
|
|
32
|
+
const node: GraphNode = {_: {"#": soul, ">": stateVector}}
|
|
33
33
|
const signatures: Record<string, string> = {}
|
|
34
34
|
|
|
35
35
|
if (typeof lex["."] === "string") {
|
|
@@ -76,8 +76,7 @@ const Get = (lex: Lex, graph: Graph, fast?: boolean): Graph | undefined => {
|
|
|
76
76
|
if (Object.keys(signatures).length > 0) {
|
|
77
77
|
node._["s"] = signatures
|
|
78
78
|
}
|
|
79
|
-
return {
|
|
79
|
+
return {[soul]: node}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export default Get
|
|
83
|
-
|
package/ts/src/ham.ts
CHANGED
|
@@ -31,17 +31,17 @@ const Ham = (
|
|
|
31
31
|
currentState: Timestamp,
|
|
32
32
|
value: GraphValue,
|
|
33
33
|
currentValue: GraphValue,
|
|
34
|
-
signed = false
|
|
34
|
+
signed = false,
|
|
35
35
|
): HamResult => {
|
|
36
|
-
if (state < currentState) return {
|
|
36
|
+
if (state < currentState) return {historical: true}
|
|
37
37
|
|
|
38
|
-
if (state > currentState) return {
|
|
38
|
+
if (state > currentState) return {incoming: true}
|
|
39
39
|
|
|
40
40
|
// state is equal to currentState. Old data is expected to propagate through
|
|
41
41
|
// the network, but conflicting values with same signed timestamp are ignored
|
|
42
42
|
// since the owner can only create one value per timestamp.
|
|
43
43
|
if (signed && value !== currentValue) {
|
|
44
|
-
return {
|
|
44
|
+
return {historical: true}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// Lexically compare to resolve conflict (for unsigned or matching values)
|
|
@@ -52,13 +52,13 @@ const Ham = (
|
|
|
52
52
|
: currentValue
|
|
53
53
|
|
|
54
54
|
// No update required
|
|
55
|
-
if (valueStr === currentValueStr) return {
|
|
55
|
+
if (valueStr === currentValueStr) return {state: true}
|
|
56
56
|
|
|
57
57
|
// Keep the current value
|
|
58
|
-
if (valueStr < currentValueStr) return {
|
|
58
|
+
if (valueStr < currentValueStr) return {current: true}
|
|
59
59
|
|
|
60
60
|
// Otherwise update using the incoming value
|
|
61
|
-
return {
|
|
61
|
+
return {incoming: true}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
@@ -69,7 +69,8 @@ Ham.mix = async (
|
|
|
69
69
|
graph: Graph,
|
|
70
70
|
hasNode: Set<string> | undefined,
|
|
71
71
|
secure: boolean,
|
|
72
|
-
listen: ListenMap
|
|
72
|
+
listen: ListenMap,
|
|
73
|
+
maxGraphSize = MAX_GRAPH_SIZE,
|
|
73
74
|
): Promise<HamMixResult> => {
|
|
74
75
|
if (!change || typeof change !== "object") {
|
|
75
76
|
throw new TypeError("change must be an object")
|
|
@@ -137,7 +138,9 @@ Ham.mix = async (
|
|
|
137
138
|
propsForTimestamp.length > 0 &&
|
|
138
139
|
propsForTimestamp.every(([prop]) => {
|
|
139
140
|
const graphState =
|
|
140
|
-
graph[soul] && graph[soul]!._[">"]
|
|
141
|
+
graph[soul] && graph[soul]!._[">"]
|
|
142
|
+
? graph[soul]!._[">"][prop]
|
|
143
|
+
: 0
|
|
141
144
|
return graphState !== undefined && graphState > asNumber
|
|
142
145
|
})
|
|
143
146
|
|
|
@@ -145,7 +148,11 @@ Ham.mix = async (
|
|
|
145
148
|
continue
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
const isValid = await SEA.verifyTimestamp(
|
|
151
|
+
const isValid = await SEA.verifyTimestamp(
|
|
152
|
+
asNumber,
|
|
153
|
+
sig as string,
|
|
154
|
+
pub,
|
|
155
|
+
)
|
|
149
156
|
if (isValid) {
|
|
150
157
|
signedTimestamps.add(asNumber)
|
|
151
158
|
for (const [prop, ts] of incomingTimestamps) {
|
|
@@ -175,7 +182,7 @@ Ham.mix = async (
|
|
|
175
182
|
const value = node[key]!
|
|
176
183
|
const state = (node._ && node._[">"] ? node._[">"][key] : 0) || 0
|
|
177
184
|
const currentValue = graph[soul] ? graph[soul][key] : undefined
|
|
178
|
-
const currentState = graph[soul] ?
|
|
185
|
+
const currentState = graph[soul] ? graph[soul]._[">"][key] || 0 : 0
|
|
179
186
|
|
|
180
187
|
if (alias && key !== utils.rel.is(value)) {
|
|
181
188
|
console.log(`error alias ${alias}: ${key} !== ${utils.rel.is(value)}`)
|
|
@@ -190,7 +197,9 @@ Ham.mix = async (
|
|
|
190
197
|
if (!defer[soul]) {
|
|
191
198
|
const stateVector: Record<string, number> = {}
|
|
192
199
|
const signatures: Record<string, string> | undefined = {}
|
|
193
|
-
defer[soul] = {
|
|
200
|
+
defer[soul] = {
|
|
201
|
+
_: {"#": soul, ">": stateVector, s: signatures},
|
|
202
|
+
} as GraphNode
|
|
194
203
|
}
|
|
195
204
|
defer[soul]![key] = value
|
|
196
205
|
defer[soul]!._[">"][key] = state
|
|
@@ -200,28 +209,39 @@ Ham.mix = async (
|
|
|
200
209
|
} else {
|
|
201
210
|
const isSigned =
|
|
202
211
|
validTimestamps.has(soul) && validTimestamps.get(soul)!.has(state)
|
|
203
|
-
const result = Ham(
|
|
212
|
+
const result = Ham(
|
|
213
|
+
state,
|
|
214
|
+
currentState,
|
|
215
|
+
value,
|
|
216
|
+
currentValue ?? null,
|
|
217
|
+
isSigned,
|
|
218
|
+
)
|
|
204
219
|
if (result.incoming) {
|
|
205
220
|
if (!now[soul]) {
|
|
206
221
|
const stateVector1: Record<string, number> = {}
|
|
207
222
|
const signatures1: Record<string, string> | undefined = {}
|
|
208
|
-
now[soul] = {
|
|
223
|
+
now[soul] = {
|
|
224
|
+
_: {"#": soul, ">": stateVector1, s: signatures1},
|
|
225
|
+
} as GraphNode
|
|
209
226
|
}
|
|
210
227
|
if (!graph[soul]) {
|
|
211
228
|
const stateVector2: Record<string, number> = {}
|
|
212
229
|
const signatures2: Record<string, string> | undefined = {}
|
|
213
|
-
graph[soul] = {
|
|
230
|
+
graph[soul] = {
|
|
231
|
+
_: {"#": soul, ">": stateVector2, s: signatures2},
|
|
232
|
+
} as GraphNode
|
|
214
233
|
}
|
|
215
234
|
graph[soul]![key] = now[soul]![key] = value
|
|
216
235
|
graph[soul]!._[">"][key] = now[soul]!._[">"][key] = state
|
|
217
236
|
if (node._["s"] && node._["s"][state]) {
|
|
218
|
-
graph[soul]!._["s"]![state] = now[soul]!._["s"]![state] =
|
|
237
|
+
graph[soul]!._["s"]![state] = now[soul]!._["s"]![state] =
|
|
238
|
+
node._["s"][state]
|
|
219
239
|
}
|
|
220
240
|
|
|
221
241
|
if (listen[soul]) {
|
|
222
|
-
listen[soul]
|
|
223
|
-
.
|
|
224
|
-
|
|
242
|
+
listen[soul]!.filter(l => utils.match(l["."], key)).forEach(l =>
|
|
243
|
+
listeners.push(l.cb as () => void),
|
|
244
|
+
)
|
|
225
245
|
}
|
|
226
246
|
updated = true
|
|
227
247
|
}
|
|
@@ -229,30 +249,29 @@ Ham.mix = async (
|
|
|
229
249
|
}
|
|
230
250
|
|
|
231
251
|
if (updated && listen[soul]) {
|
|
232
|
-
listen[soul]
|
|
233
|
-
.
|
|
234
|
-
|
|
252
|
+
listen[soul]!.filter(l => utils.match(l["."])).forEach(l =>
|
|
253
|
+
listeners.push(l.cb as () => void),
|
|
254
|
+
)
|
|
235
255
|
}
|
|
236
256
|
}
|
|
237
257
|
|
|
238
258
|
const souls = Object.keys(graph)
|
|
239
|
-
if (souls.length >
|
|
259
|
+
if (souls.length > maxGraphSize) {
|
|
240
260
|
const soulsByAge = souls
|
|
241
261
|
.map(soul => {
|
|
242
262
|
const states = graph[soul]!._ && graph[soul]!._[">"]
|
|
243
263
|
const maxState = states ? Math.max(...Object.values(states)) : 0
|
|
244
|
-
return {
|
|
264
|
+
return {soul, maxState}
|
|
245
265
|
})
|
|
246
266
|
.sort((a, b) => a.maxState - b.maxState)
|
|
247
|
-
const remove = soulsByAge.slice(0, souls.length -
|
|
248
|
-
remove.forEach(({
|
|
267
|
+
const remove = soulsByAge.slice(0, souls.length - maxGraphSize)
|
|
268
|
+
remove.forEach(({soul}) => {
|
|
249
269
|
delete graph[soul]
|
|
250
270
|
if (hasNode) hasNode.delete(soul)
|
|
251
271
|
})
|
|
252
272
|
}
|
|
253
273
|
|
|
254
|
-
return {
|
|
274
|
+
return {now, defer, wait, listeners}
|
|
255
275
|
}
|
|
256
276
|
|
|
257
277
|
export default Ham
|
|
258
|
-
|