@mblaney/holster 1.0.25 → 1.0.27

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mblaney/holster",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "Holster is a service that allows synchronising data between devices using Node, Deno, Bun or the browser.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/src/get.js CHANGED
@@ -1,4 +1,4 @@
1
- import {match} from "./utils.js"
1
+ import {match, userPublicKey} from "./utils.js"
2
2
 
3
3
  const Get = (lex, graph) => {
4
4
  if (!lex || typeof lex !== "object") {
@@ -34,7 +34,8 @@ const Get = (lex, graph) => {
34
34
  }
35
35
  } else {
36
36
  for (const key of Object.keys(graph[soul])) {
37
- if (match(lex["."], key)) {
37
+ // Always include userPublicKey for verification, regardless of filter
38
+ if (key === userPublicKey || match(lex["."], key)) {
38
39
  node[key] = graph[soul][key]
39
40
  node._[">"][key] = graph[soul]._[">"][key]
40
41
  const state = graph[soul]._[">"][key]
package/src/ham.js CHANGED
@@ -95,6 +95,8 @@ Ham.mix = async (change, graph, secure, listen) => {
95
95
 
96
96
  const signedTimestamps = new Set()
97
97
  const signedProperties = new Set()
98
+
99
+ // Verify timestamp signatures (numeric keys in node._["s"])
98
100
  for (const [key, sig] of Object.entries(node._["s"] || {})) {
99
101
  const asNumber = Number(key)
100
102
  if (!isNaN(asNumber) && asNumber > 0) {
@@ -103,7 +105,7 @@ Ham.mix = async (change, graph, secure, listen) => {
103
105
  signedTimestamps.add(asNumber)
104
106
  // Add all properties with this timestamp to signed set
105
107
  for (const [prop, ts] of Object.entries(node._[">"] || {})) {
106
- if (ts === asNumber) {
108
+ if (Number(ts) === asNumber) {
107
109
  signedProperties.add(prop)
108
110
  }
109
111
  }
@@ -111,6 +113,10 @@ Ham.mix = async (change, graph, secure, listen) => {
111
113
  }
112
114
  }
113
115
 
116
+ // Verify per-property signatures (backward compatibility)
117
+ const perPropertySignedProps = await SEA.verifyProperties(node, pub)
118
+ perPropertySignedProps.forEach(prop => signedProperties.add(prop))
119
+
114
120
  // If no properties verified, skip this update
115
121
  if (signedProperties.size === 0) {
116
122
  continue
package/src/radisk.js CHANGED
@@ -34,6 +34,7 @@ const Radisk = opt => {
34
34
  if (!opt.write) opt.write = 1 // Wait time before write in milliseconds
35
35
  if (!opt.size) opt.size = 1024 * 1024 // File size on disk, default 1MB
36
36
  if (!opt.memoryLimit) opt.memoryLimit = 500 // Memory limit in MB
37
+ if (!opt.readTimeout) opt.readTimeout = 1000 // Read timeout in milliseconds
37
38
  if (typeof opt.cache === "undefined") opt.cache = true
38
39
 
39
40
  if (!opt.store) {
@@ -126,20 +127,34 @@ const Radisk = opt => {
126
127
  // Check if there's already a pending read for this key
127
128
  if (pendingReads.has(key)) {
128
129
  // Add callback to existing pending read
129
- pendingReads.get(key).push(cb)
130
+ pendingReads.get(key).callbacks.push(cb)
130
131
  return
131
132
  }
132
133
 
133
- // Start new read and create callback queue
134
- pendingReads.set(key, [cb])
134
+ // Start new read and create callback queue with fired flag
135
+ const pending = {callbacks: [cb], fired: false, timeoutId: null}
136
+ pendingReads.set(key, pending)
137
+
138
+ // Set a timeout to fire callbacks if read takes too long
139
+ pending.timeoutId = setTimeout(() => {
140
+ if (!pending.fired) {
141
+ pending.fired = true
142
+ pendingReads.delete(key)
143
+ const err = new Error("radisk read timeout")
144
+ pending.callbacks.forEach(callback => callback(err, undefined))
145
+ }
146
+ }, opt.readTimeout)
135
147
 
136
148
  const readStart = Date.now()
137
149
  return radisk.read(key, (err, result) => {
150
+ clearTimeout(pending.timeoutId)
138
151
  perfLog("read", readStart, key)
139
- // Execute all pending callbacks for this key
140
- const callbacks = pendingReads.get(key) || []
141
- pendingReads.delete(key)
142
- callbacks.forEach(callback => callback(err, result))
152
+ // Execute all pending callbacks for this key if timeout hasn't fired
153
+ if (!pending.fired) {
154
+ pending.fired = true
155
+ pendingReads.delete(key)
156
+ pending.callbacks.forEach(callback => callback(err, result))
157
+ }
143
158
  })
144
159
  }
145
160
 
package/src/sea.js CHANGED
@@ -238,6 +238,59 @@ const SEA = {
238
238
  return false
239
239
  }
240
240
  },
241
+ // Verify individual property signatures stored in node._["s"]
242
+ // Returns array of valid property names
243
+ verifyProperties: async (node, pub, cb) => {
244
+ if (!pub || !node) {
245
+ if (cb) cb([])
246
+ return []
247
+ }
248
+
249
+ const key = await utils.subtle.importKey(
250
+ "jwk",
251
+ utils.jwk(pub),
252
+ {name: "ECDSA", namedCurve: "P-256"},
253
+ false,
254
+ ["verify"],
255
+ )
256
+
257
+ const alg = {name: "ECDSA", hash: {name: "SHA-256"}}
258
+ const valid = []
259
+ const propertySignatures = (node._ && node._["s"]) || {}
260
+
261
+ // Verify each property individually
262
+ for (const k of Object.keys(node).sort()) {
263
+ if (k === "_" || k == userPublicKey) continue
264
+
265
+ const propSig = propertySignatures[k]
266
+ if (!propSig) {
267
+ console.log(`warning: property '${k}' missing signature`)
268
+ continue
269
+ }
270
+
271
+ try {
272
+ const hash = await utils.sha256(node[k])
273
+ const sig = new Uint8Array(SafeBuffer.from(propSig, "base64"))
274
+
275
+ const isValid = await utils.subtle.verify(
276
+ alg,
277
+ key,
278
+ sig,
279
+ new Uint8Array(hash),
280
+ )
281
+ if (isValid) {
282
+ valid.push(k)
283
+ } else {
284
+ console.log(`warning: property '${k}' signature verification failed`)
285
+ }
286
+ } catch (err) {
287
+ console.log(`warning: property '${k}' error verifying: ${err.message}`)
288
+ }
289
+ }
290
+
291
+ if (cb) cb(valid)
292
+ return valid
293
+ },
241
294
  work: async (data, salt, cb) => {
242
295
  if (typeof salt === "function") {
243
296
  cb = salt
package/src/store.js CHANGED
@@ -226,7 +226,8 @@ const Store = opt => {
226
226
  var node
227
227
  var signatures = {}
228
228
  const each = (value, key) => {
229
- if (!utils.match(lex["."], key)) return
229
+ // Always include userPublicKey for verification, regardless of filter
230
+ if (key !== utils.userPublicKey && !utils.match(lex["."], key)) return
230
231
 
231
232
  if (!node) node = {_: {"#": soul, ">": {}}}
232
233
  node[key] = value[0]
package/src/wire.js CHANGED
@@ -302,10 +302,9 @@ const Wire = opt => {
302
302
 
303
303
  const ack = Get(lex, graph)
304
304
  const track = utils.text.random(9)
305
- // Request the whole node in secure mode for verification.
306
305
  const request = JSON.stringify({
307
306
  "#": dup.track(track),
308
- get: opt.secure ? {"#": lex["#"]} : lex,
307
+ get: lex,
309
308
  })
310
309
 
311
310
  if (ack) {