@mblaney/holster 1.0.0 → 1.0.2

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.0",
3
+ "version": "1.0.2",
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/ham.js CHANGED
@@ -63,11 +63,10 @@ Ham.mix = async (change, graph, secure, listen) => {
63
63
  }
64
64
  }
65
65
  if (verify) {
66
- // Partial nodes can be read from disk, just ignore them.
67
- if (!sig || !pub) continue
68
-
69
- if (!(await SEA.verify({m: node, s: sig}, {pub: pub}))) {
70
- console.log(`error could not verify soul: ${soul}`)
66
+ // Partial nodes can be read from disk, which are ignored. This could be
67
+ // a missing signature or public key, or missing properties on the node
68
+ // which means verify won't work either.
69
+ if (!sig || !pub || !(await SEA.verify({m: node, s: sig}, {pub: pub}))) {
71
70
  continue
72
71
  }
73
72
  }
package/src/holster.js CHANGED
@@ -100,17 +100,21 @@ const Holster = opt => {
100
100
  return utils.graph(soul, data)
101
101
  }
102
102
 
103
- const done = data => {
104
- const ctx = allctx.get(ctxid)
105
- if (ctx && typeof ctx.cb !== "undefined") {
106
- // Use a timeout so that the context can be removed before data is
107
- // returned to the callback (allows nested get calls).
108
- setTimeout(() => ctx.cb(data), 1)
109
- } else if (data) {
110
- console.log("error no callback for data", data, "ctx", ctx)
103
+ // done takes a context id and returns a new callback function so that the
104
+ // callback is not overwritten by simultaneous requests.
105
+ const done = ctxid => {
106
+ return data => {
107
+ const ctx = allctx.get(ctxid)
108
+ if (ctx && typeof ctx.cb !== "undefined") {
109
+ // Use a timeout so that the context can be removed before data is
110
+ // returned to the callback (allows nested get calls).
111
+ setTimeout(() => ctx.cb(data), 1)
112
+ } else if (data) {
113
+ console.log("error no callback for data", data, "ctx", ctx)
114
+ }
115
+ // A context updated by "on" should only be removed by "off".
116
+ if (ctx && !ctx.on) allctx.delete(ctxid)
111
117
  }
112
- // A context updated by "on" should only be removed by "off".
113
- if (ctx && !ctx.on) allctx.delete(ctxid)
114
118
  }
115
119
 
116
120
  const resolve = (request, cb) => {
@@ -225,7 +229,7 @@ const Holster = opt => {
225
229
  // The context has been resolved but it does not include the requested
226
230
  // node, which requires one more lookup.
227
231
  ctx.chain.push({item: null, soul: null})
228
- api(ctxid).next(null, request.get, cb)
232
+ api(ctxid).next(null, request.get, cb, request._opt)
229
233
  return false
230
234
  }
231
235
 
@@ -258,13 +262,22 @@ const Holster = opt => {
258
262
  allctx.set(ctxid, {chain: [{item: key, soul: "root"}], cb: cb})
259
263
  if (!cb) return api(ctxid)
260
264
 
265
+ const _done = done(ctxid)
261
266
  // When there's a callback need to resolve the context first.
262
- const {soul} = resolve({get: lex, _opt: _opt}, done)
263
- if (soul) get(lex, soul, done, _opt)
267
+ const {soul} = resolve({get: lex, _opt: _opt}, _done)
268
+ if (soul) get(lex, soul, _done, _opt)
264
269
  },
265
270
  next: (key, lex, cb, _opt) => {
266
- const ack = data => {
267
- cb ? cb(data) : done(data)
271
+ // ack needs to work the same as done, pass it the context id and then
272
+ // return a new function for the actual callback.
273
+ const ack = ctxid => {
274
+ return data => {
275
+ if (cb) {
276
+ cb(data)
277
+ } else {
278
+ done(ctxid)(data)
279
+ }
280
+ }
268
281
  }
269
282
  if (typeof lex === "function") {
270
283
  _opt = cb
@@ -273,12 +286,13 @@ const Holster = opt => {
273
286
  }
274
287
  if (!ctxid) {
275
288
  console.log("error please provide a key using get(key)")
276
- ack(null)
289
+ if (cb) cb(null)
277
290
  return
278
291
  }
279
292
 
293
+ const _ack = ack(ctxid)
280
294
  if (key === "" || key === "_") {
281
- ack(null)
295
+ _ack(null)
282
296
  return
283
297
  }
284
298
 
@@ -304,20 +318,25 @@ const Holster = opt => {
304
318
  if (!ctx.cb) return api(ctxid)
305
319
 
306
320
  // When there's a callback need to resolve the context first.
307
- const {soul} = resolve({get: lex, _opt: _opt}, ack)
308
- if (soul) get(lex, soul, ack, _opt)
321
+ const {soul} = resolve({get: lex, _opt: _opt}, _ack)
322
+ if (soul) get(lex, soul, _ack, _opt)
309
323
  },
310
324
  put: (data, set, cb) => {
311
325
  if (typeof set === "function") {
312
326
  cb = set
313
327
  set = false
314
328
  }
315
- const ack = err => {
316
- cb ? cb(err) : done(err)
329
+ const ack = ctxid => {
330
+ return data => {
331
+ if (cb) {
332
+ cb(data)
333
+ } else {
334
+ done(ctxid)(data)
335
+ }
336
+ }
317
337
  }
318
-
319
338
  if (!ctxid) {
320
- ack("please provide a key using get(key)")
339
+ if (cb) cb("error please provide a key using get(key)")
321
340
  return
322
341
  }
323
342
 
@@ -334,15 +353,17 @@ const Holster = opt => {
334
353
  }
335
354
  if (set) data = {[utils.text.random()]: data}
336
355
 
356
+ const _ack = ack(ctxid)
337
357
  const result = check(data)
338
358
  if (typeof result === "string") {
339
359
  // All strings returned from check are errors, cannot continue.
340
- ack(result)
360
+ _ack(result)
341
361
  return
342
362
  }
343
363
 
344
- // Resolve the current context before putting data.
345
- const {item, soul} = resolve({put: data}, ack)
364
+ // Resolve the current context before putting data. (Note that set is
365
+ // not passed to resolve because it's already been applied above.)
366
+ const {item, soul} = resolve({put: data}, _ack)
346
367
  if (!soul) return
347
368
 
348
369
  if (result === true) {
@@ -354,7 +375,7 @@ const Holster = opt => {
354
375
  ctx.user || opt.secure ? {"#": soul} : {"#": soul, ".": item}
355
376
  wire.get(lex, async msg => {
356
377
  if (msg.err) {
357
- ack(`error getting ${soul}: ${msg.err}`)
378
+ _ack(`error getting ${soul}: ${msg.err}`)
358
379
  return
359
380
  }
360
381
 
@@ -365,21 +386,21 @@ const Holster = opt => {
365
386
  // Not a rel, can just put the data.
366
387
  if (!node) node = {}
367
388
  node[item] = data
368
- const g = await graph(soul, node, ctx.user, ack)
389
+ const g = await graph(soul, node, ctx.user, _ack)
369
390
  if (g === null) return
370
391
 
371
- wire.put(g, ack)
392
+ wire.put(g, _ack)
372
393
  return
373
394
  }
374
395
 
375
396
  wire.get({"#": id}, async msg => {
376
397
  if (msg.err) {
377
- ack(`error getting ${id}: ${msg.err}`)
398
+ _ack(`error getting ${id}: ${msg.err}`)
378
399
  return
379
400
  }
380
401
 
381
402
  if (!msg.put || !msg.put[id]) {
382
- ack(`error ${id} not found`)
403
+ _ack(`error ${id} not found`)
383
404
  return
384
405
  }
385
406
 
@@ -400,16 +421,16 @@ const Holster = opt => {
400
421
  api(_ctxid).put(null, res)
401
422
  })
402
423
  if (err) {
403
- ack(err)
424
+ _ack(err)
404
425
  return
405
426
  }
406
427
  }
407
428
  if (!node) node = {}
408
429
  node[item] = data
409
- const g = await graph(soul, node, ctx.user, ack)
430
+ const g = await graph(soul, node, ctx.user, _ack)
410
431
  if (g === null) return
411
432
 
412
- wire.put(g, ack)
433
+ wire.put(g, _ack)
413
434
  })
414
435
  })
415
436
  return
@@ -422,7 +443,7 @@ const Holster = opt => {
422
443
  ctx.user || opt.secure ? {"#": soul} : {"#": soul, ".": item}
423
444
  wire.get(lex, async msg => {
424
445
  if (msg.err) {
425
- ack(`error getting ${soul}.${item}: ${msg.err}`)
446
+ _ack(`error getting ${soul}.${item}: ${msg.err}`)
426
447
  return
427
448
  }
428
449
 
@@ -433,12 +454,12 @@ const Holster = opt => {
433
454
  // The current rel doesn't exist, so add it first.
434
455
  if (!node) node = {}
435
456
  node[item] = utils.rel.ify(utils.text.random())
436
- const g = await graph(soul, node, ctx.user, ack)
457
+ const g = await graph(soul, node, ctx.user, _ack)
437
458
  if (g === null) return
438
459
 
439
460
  wire.put(g, err => {
440
461
  if (err) {
441
- ack(`error putting ${item} on ${soul}: ${err}`)
462
+ _ack(`error putting ${item} on ${soul}: ${err}`)
442
463
  } else {
443
464
  const _ctxid = utils.text.random()
444
465
  const chain = [{item: item, soul: soul}]
@@ -466,13 +487,13 @@ const Holster = opt => {
466
487
  }
467
488
  })
468
489
  if (err) {
469
- ack(err)
490
+ _ack(err, ctxid)
470
491
  return
471
492
  }
472
493
  }
473
494
 
474
495
  if (update.length === 0) {
475
- ack(null)
496
+ _ack(null)
476
497
  return
477
498
  }
478
499
 
@@ -480,7 +501,7 @@ const Holster = opt => {
480
501
  // object, so fetch the node so the rest of the updates can be added.
481
502
  wire.get({"#": id}, async msg => {
482
503
  if (msg.err) {
483
- ack(`error getting ${id}: ${msg.err}`)
504
+ _ack(`error getting ${id}: ${msg.err}`)
484
505
  return
485
506
  }
486
507
 
@@ -489,10 +510,10 @@ const Holster = opt => {
489
510
  update.forEach(key => {
490
511
  node[key] = data[key]
491
512
  })
492
- const g = await graph(id, node, ctx.user, ack)
513
+ const g = await graph(id, node, ctx.user, _ack)
493
514
  if (g === null) return
494
515
 
495
- wire.put(g, ack)
516
+ wire.put(g, _ack)
496
517
  })
497
518
  })
498
519
  },
@@ -618,8 +639,9 @@ const Holster = opt => {
618
639
  if (!cb) return api(ctxid)
619
640
 
620
641
  // When there's a callback need to resolve the context first.
621
- const {soul} = resolve({get: lex}, done)
622
- if (soul) get(lex, soul, done, _opt)
642
+ const _done = done(ctxid)
643
+ const {soul} = resolve({get: lex, _opt: _opt}, _done)
644
+ if (soul) get(lex, soul, _done, _opt)
623
645
  }
624
646
  }
625
647
  return user
package/src/user.js CHANGED
@@ -27,53 +27,57 @@ const User = (opt, wire) => {
27
27
  }
28
28
 
29
29
  const pub = pubs.shift()
30
- wire.get({"#": pub}, async msg => {
31
- if (msg.err) {
32
- done(`error getting ${pub}: ${msg.err}`)
33
- return
34
- }
30
+ wire.get(
31
+ {"#": pub},
32
+ async msg => {
33
+ if (msg.err) {
34
+ done(`error getting ${pub}: ${msg.err}`)
35
+ return
36
+ }
35
37
 
36
- const data = msg.put && msg.put[pub]
37
- if (!data || !data.auth) return next()
38
+ const data = msg.put && msg.put[pub]
39
+ if (!data || !data.auth) return next()
38
40
 
39
- const auth = JSON.parse(data.auth)
40
- const work = await SEA.work(password, auth.salt)
41
- const dec = await SEA.decrypt(auth.enc, work)
42
- if (!dec) return next()
41
+ const auth = JSON.parse(data.auth)
42
+ const work = await SEA.work(password, auth.salt)
43
+ const dec = await SEA.decrypt(auth.enc, work)
44
+ if (!dec) return next()
43
45
 
44
- user.is = {
45
- username: username,
46
- pub: data.pub,
47
- epub: data.epub,
48
- priv: dec.priv,
49
- epriv: dec.epriv,
50
- }
51
- if (newPassword !== "") {
52
- // Encrypt private key using new password and a new salt.
53
- const salt = utils.text.random(64)
54
- const work = await SEA.work(newPassword, salt)
55
- const enc = await SEA.encrypt(dec, work)
56
- // Put all fields so that verification applies to the whole node.
57
- const update = {
46
+ user.is = {
58
47
  username: username,
59
48
  pub: data.pub,
60
49
  epub: data.epub,
61
- auth: JSON.stringify({enc: enc, salt: salt}),
50
+ priv: dec.priv,
51
+ epriv: dec.epriv,
62
52
  }
63
- const signed = await SEA.sign(update, user.is)
64
- const graph = utils.graph(pub, signed.m, signed.s, data.pub)
65
- wire.put(graph, err => {
66
- if (err) {
67
- done(`error putting ${update} on ${pub}: ${err}`)
68
- } else {
69
- done(null)
53
+ if (newPassword !== "") {
54
+ // Encrypt private key using new password and a new salt.
55
+ const salt = utils.text.random(64)
56
+ const work = await SEA.work(newPassword, salt)
57
+ const enc = await SEA.encrypt(dec, work)
58
+ // Put all fields so that verification applies to the whole node.
59
+ const update = {
60
+ username: username,
61
+ pub: data.pub,
62
+ epub: data.epub,
63
+ auth: JSON.stringify({enc: enc, salt: salt}),
70
64
  }
71
- })
72
- return
73
- }
65
+ const signed = await SEA.sign(update, user.is)
66
+ const graph = utils.graph(pub, signed.m, signed.s, data.pub)
67
+ wire.put(graph, err => {
68
+ if (err) {
69
+ done(`error putting ${update} on ${pub}: ${err}`)
70
+ } else {
71
+ done(null)
72
+ }
73
+ })
74
+ return
75
+ }
74
76
 
75
- return done(null)
76
- })
77
+ return done(null)
78
+ },
79
+ {wait: 1000},
80
+ )
77
81
  }
78
82
 
79
83
  if (retries > 9) {
@@ -82,21 +86,25 @@ const User = (opt, wire) => {
82
86
  }
83
87
 
84
88
  const soul = "~@" + username
85
- wire.get({"#": soul}, async msg => {
86
- if (msg.err) {
87
- done(`error getting ${soul}: ${msg.err}`)
88
- return
89
- }
90
-
91
- const data = msg.put && msg.put[soul]
92
- if (!data) return retry(username)
89
+ wire.get(
90
+ {"#": soul},
91
+ async msg => {
92
+ if (msg.err) {
93
+ done(`error getting ${soul}: ${msg.err}`)
94
+ return
95
+ }
93
96
 
94
- delete msg.put[soul]._
95
- // Usernames aren't guaranteed to be unique, so store the list and then
96
- // try each of them until one is successful with the given password.
97
- pubs = Object.keys(data)
98
- next()
99
- })
97
+ const data = msg.put && msg.put[soul]
98
+ if (!data) return retry(username)
99
+
100
+ delete msg.put[soul]._
101
+ // Usernames aren't guaranteed to be unique, so store the list and then
102
+ // try each of them until one is successful with the given password.
103
+ pubs = Object.keys(data)
104
+ next()
105
+ },
106
+ {wait: 1000},
107
+ )
100
108
  }
101
109
 
102
110
  const user = {
@@ -124,53 +132,57 @@ const User = (opt, wire) => {
124
132
  creating = true
125
133
 
126
134
  const soul = "~@" + username
127
- wire.get({"#": soul}, async msg => {
128
- if (msg.err) {
129
- creating = false
130
- ack(`error getting ${soul}: ${msg.err}`)
131
- return
132
- }
133
-
134
- if (msg.put && msg.put[soul]) {
135
- creating = false
136
- ack("Username already exists")
137
- return
138
- }
139
-
140
- const salt = utils.text.random(64)
141
- const work = await SEA.work(password, salt)
142
- const pair = await SEA.pair()
143
- const priv = {priv: pair.priv, epriv: pair.epriv}
144
- const enc = await SEA.encrypt(priv, work)
145
- const data = {
146
- username: username,
147
- pub: pair.pub,
148
- epub: pair.epub,
149
- auth: JSON.stringify({enc: enc, salt: salt}),
150
- }
135
+ wire.get(
136
+ {"#": soul},
137
+ async msg => {
138
+ if (msg.err) {
139
+ creating = false
140
+ ack(`error getting ${soul}: ${msg.err}`)
141
+ return
142
+ }
151
143
 
152
- const pub = "~" + pair.pub
153
- const signed = await SEA.sign(data, pair)
154
- const graph = utils.graph(pub, signed.m, signed.s, pair.pub)
155
- wire.put(graph, err => {
156
- creating = false
157
- if (err) {
158
- ack(`error putting ${data} on ${pub}: ${err}`)
144
+ if (msg.put && msg.put[soul]) {
145
+ creating = false
146
+ ack("Username already exists")
159
147
  return
160
148
  }
161
149
 
162
- const rel = {[pub]: {"#": pub}}
163
- wire.put(utils.graph(soul, rel), err => {
150
+ const salt = utils.text.random(64)
151
+ const work = await SEA.work(password, salt)
152
+ const pair = await SEA.pair()
153
+ const priv = {priv: pair.priv, epriv: pair.epriv}
154
+ const enc = await SEA.encrypt(priv, work)
155
+ const data = {
156
+ username: username,
157
+ pub: pair.pub,
158
+ epub: pair.epub,
159
+ auth: JSON.stringify({enc: enc, salt: salt}),
160
+ }
161
+
162
+ const pub = "~" + pair.pub
163
+ const signed = await SEA.sign(data, pair)
164
+ const graph = utils.graph(pub, signed.m, signed.s, pair.pub)
165
+ wire.put(graph, err => {
166
+ creating = false
164
167
  if (err) {
165
- ack(`error putting ${rel} on ${soul}: ${err}`)
168
+ ack(`error putting ${data} on ${pub}: ${err}`)
166
169
  return
167
170
  }
168
171
 
169
- // Return null on success.
170
- if (cb) cb(null)
172
+ const rel = {[pub]: {"#": pub}}
173
+ wire.put(utils.graph(soul, rel), err => {
174
+ if (err) {
175
+ ack(`error putting ${rel} on ${soul}: ${err}`)
176
+ return
177
+ }
178
+
179
+ // Return null on success.
180
+ if (cb) cb(null)
181
+ })
171
182
  })
172
- })
173
- })
183
+ },
184
+ {wait: 1000},
185
+ )
174
186
  },
175
187
  auth: (username, password, cb) => {
176
188
  const ack = err => {
@@ -37,12 +37,16 @@ describe("holster.get", () => {
37
37
  })
38
38
 
39
39
  test("get unknown keys in for loop callbacks null", (t, done) => {
40
+ let count = 0
40
41
  for (let i = 0; i < 5; i++) {
41
42
  holster.get("unknown" + i, data => {
42
43
  assert.equal(data, null)
43
- if (i === 4) done()
44
+ count++
44
45
  })
45
46
  }
47
+ setTimeout(() => {
48
+ if (count === 5) done()
49
+ }, 200)
46
50
  })
47
51
 
48
52
  test("nested unknown keys both callbacks null", (t, done) => {
@@ -71,12 +71,16 @@ describe("holster.put", () => {
71
71
  }
72
72
 
73
73
  setTimeout(() => {
74
+ let count = 0
74
75
  for (let i = 0; i < 5; i++) {
75
76
  holster.get("for" + i, data => {
76
77
  assert.equal(data, i)
77
- if (i === 4) setTimeout(done, 100)
78
+ count++
78
79
  })
79
80
  }
81
+ setTimeout(() => {
82
+ if (count === 5) done()
83
+ }, 200)
80
84
  }, 200)
81
85
  })
82
86
 
@@ -63,12 +63,16 @@ describe("holster.user.get", () => {
63
63
  })
64
64
 
65
65
  test("get unknown keys in for loop callbacks null", (t, done) => {
66
+ let count = 0
66
67
  for (let i = 0; i < 5; i++) {
67
68
  user.get("unknown" + i, data => {
68
69
  assert.equal(data, null)
69
- if (i === 4) done()
70
+ count++
70
71
  })
71
72
  }
73
+ setTimeout(() => {
74
+ if (count === 5) done()
75
+ }, 200)
72
76
  })
73
77
 
74
78
  test("nested get unknown key callback null", (t, done) => {
@@ -101,11 +101,12 @@ describe("holster.user.on", () => {
101
101
 
102
102
  // Need to wait until after all initial puts are done to set on listeners.
103
103
  setTimeout(async () => {
104
+ let count = 0
104
105
  for (let i = 0; i < 5; i++) {
105
106
  user.get("for" + i).on(data => {
106
107
  assert.equal(data, "update" + i)
107
108
  user.get("for" + i).off()
108
- if (i === 4) done()
109
+ count++
109
110
  })
110
111
  }
111
112
 
@@ -117,6 +118,9 @@ describe("holster.user.on", () => {
117
118
  })
118
119
  assert.equal(err, null)
119
120
  }
121
+ setTimeout(() => {
122
+ if (count === 5) done()
123
+ }, 500)
120
124
  }, 500)
121
125
  }, 1000)
122
126
  })
@@ -98,12 +98,16 @@ describe("holster.user.put", () => {
98
98
  })()
99
99
 
100
100
  setTimeout(async () => {
101
+ let count = 0
101
102
  for (let i = 0; i < 5; i++) {
102
103
  user.get("for" + i, data => {
103
104
  assert.equal(data, i)
104
- if (i === 4) setTimeout(done, 100)
105
+ count++
105
106
  })
106
107
  }
108
+ setTimeout(() => {
109
+ if (count === 5) done()
110
+ }, 200)
107
111
  }, 500)
108
112
  })
109
113