@mblaney/holster 0.3.0 → 1.0.1

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/src/user.js CHANGED
@@ -48,20 +48,23 @@ const User = (opt, wire) => {
48
48
  priv: dec.priv,
49
49
  epriv: dec.epriv,
50
50
  }
51
- // Holster API uses ctx to set the root node for user data.
52
- user.ctx = "~" + data.pub
53
-
54
51
  if (newPassword !== "") {
55
52
  // Encrypt private key using new password and a new salt.
56
53
  const salt = utils.text.random(64)
57
54
  const work = await SEA.work(newPassword, salt)
58
55
  const enc = await SEA.encrypt(dec, work)
59
- const auth = {auth: JSON.stringify({enc: enc, salt: salt})}
60
- const signed = await SEA.sign(auth, user.is)
56
+ // Put all fields so that verification applies to the whole node.
57
+ const update = {
58
+ username: username,
59
+ pub: data.pub,
60
+ epub: data.epub,
61
+ auth: JSON.stringify({enc: enc, salt: salt}),
62
+ }
63
+ const signed = await SEA.sign(update, user.is)
61
64
  const graph = utils.graph(pub, signed.m, signed.s, data.pub)
62
65
  wire.put(graph, err => {
63
66
  if (err) {
64
- done(`error putting ${auth} on ${pub}: ${err}`)
67
+ done(`error putting ${update} on ${pub}: ${err}`)
65
68
  } else {
66
69
  done(null)
67
70
  }
@@ -236,40 +239,37 @@ const User = (opt, wire) => {
236
239
  if (typeof globalThis.localStorage !== "undefined") {
237
240
  globalThis.localStorage.setItem("user.is", JSON.stringify(user.is))
238
241
  }
242
+ if (typeof globalThis.sessionStorage !== "undefined") {
243
+ globalThis.sessionStorage.removeItem("user.is")
244
+ }
239
245
  return
240
246
  }
241
247
 
242
248
  if (typeof globalThis.sessionStorage !== "undefined") {
243
249
  globalThis.sessionStorage.setItem("user.is", JSON.stringify(user.is))
244
250
  }
251
+ if (typeof globalThis.localStorage !== "undefined") {
252
+ globalThis.localStorage.removeItem("user.is")
253
+ }
245
254
  },
246
- recall: localStorage => {
247
- if (localStorage) {
248
- if (typeof globalThis.localStorage !== "undefined") {
249
- const is = globalThis.localStorage.getItem("user.is")
250
- if (is) {
251
- user.is = JSON.parse(is)
252
- user.ctx = "~" + user.is.pub
253
- } else {
254
- console.log("User credentials not stored in local storage")
255
- }
255
+ recall: () => {
256
+ if (typeof globalThis.localStorage !== "undefined") {
257
+ const is = globalThis.localStorage.getItem("user.is")
258
+ if (is) {
259
+ user.is = JSON.parse(is)
260
+ return
256
261
  }
257
- return
258
262
  }
259
263
 
260
264
  if (typeof globalThis.sessionStorage !== "undefined") {
261
265
  const is = globalThis.sessionStorage.getItem("user.is")
262
266
  if (is) {
263
267
  user.is = JSON.parse(is)
264
- user.ctx = "~" + user.is.pub
265
- } else {
266
- console.log("User credentials not stored in session storage")
267
268
  }
268
269
  }
269
270
  },
270
271
  leave: () => {
271
272
  user.is = null
272
- user.ctx = null
273
273
  if (typeof globalThis.localStorage !== "undefined") {
274
274
  globalThis.localStorage.removeItem("user.is")
275
275
  }
@@ -316,7 +316,6 @@ const User = (opt, wire) => {
316
316
  }
317
317
 
318
318
  user.is = null
319
- user.ctx = null
320
319
  // Return null on success. Note currently not updating ~@username,
321
320
  // not sure if allowing username re-user is a good idea anyway?
322
321
  if (cb) cb(null)
package/src/utils.js CHANGED
@@ -68,19 +68,26 @@ export const rel = {
68
68
  ify: soul => obj.put({}, "#", soul),
69
69
  }
70
70
 
71
+ export const userSignature = "_holster_user_signature"
71
72
  export const userPublicKey = "_holster_user_public_key"
72
73
 
73
74
  // graph converts objects to graph format with updated states,
74
75
  // with optional meta data to verify signed data.
75
76
  export const graph = (soul, data, sig, pub) => {
76
- const g = {[soul]: {_: {"#": soul, ">": {}, s: sig, p: pub}}}
77
+ const g = {[soul]: {_: {"#": soul, ">": {}}}}
77
78
  for (const [key, value] of Object.entries(data)) {
78
- g[soul][key] = value
79
- g[soul]._[">"][key] = Date.now()
79
+ if (key !== "_" && key != userPublicKey && key != userSignature) {
80
+ g[soul][key] = value
81
+ g[soul]._[">"][key] = Date.now()
82
+ }
80
83
  }
81
- // If a public key is provided it also needs to be stored on the node to
82
- // ensure that future updates are only possible with the same public key.
83
- if (pub) {
84
+ // If a signature and public key are provided they also need to be stored on
85
+ // the node to ensure that future updates are only possible with the same
86
+ // public key. The signature is requried because later get requests will
87
+ // broadcast the data as a put, which other devices will need to verify.
88
+ if (sig && pub) {
89
+ g[soul][userSignature] = sig
90
+ g[soul]._[">"][userSignature] = Date.now()
84
91
  g[soul][userPublicKey] = pub
85
92
  g[soul]._[">"][userPublicKey] = Date.now()
86
93
  }
package/src/wire.js CHANGED
@@ -37,13 +37,12 @@ const Wire = opt => {
37
37
  }
38
38
 
39
39
  const node = data[soul]
40
- const pub = node._ ? node._.p : undefined
41
40
  const key = utils.userPublicKey
42
41
  // If there is no current node then the data is ok to write without
43
42
  // matching public keys, as the provided soul also needs a rel on the
44
43
  // parent node which then also requires checking. Otherwise publc keys
45
44
  // need to match for existing data.
46
- if (!msg.put || !msg.put[soul] || msg.put[soul][key] === pub) {
45
+ if (!msg.put || !msg.put[soul] || msg.put[soul][key] === node[key]) {
47
46
  continue
48
47
  }
49
48
 
@@ -107,32 +106,38 @@ const Wire = opt => {
107
106
  }
108
107
  }
109
108
 
110
- const getWithCallback = (lex, cb, send, opt) => {
109
+ const getWithCallback = (lex, cb, send, _opt) => {
111
110
  if (!cb) return
112
111
 
113
- if (!utils.obj.is(opt)) opt = {}
112
+ if (!utils.obj.is(_opt)) _opt = {}
113
+
114
114
  const ack = Get(lex, graph)
115
+ const track = utils.text.random(9)
116
+ // Request the whole node in secure mode for verification.
117
+ const request = JSON.stringify({
118
+ "#": dup.track(track),
119
+ get: opt.secure ? {"#": lex["#"]} : lex,
120
+ })
121
+
115
122
  if (ack) {
123
+ // Also send request on the wire to check for updates.
124
+ send(request)
116
125
  cb({put: ack})
117
126
  return
118
127
  }
119
128
 
120
129
  store.get(lex, (err, ack) => {
121
130
  if (ack) {
131
+ // Also send request on the wire to check for updates.
132
+ send(request)
122
133
  cb({put: ack, err: err})
123
134
  return
124
135
  }
125
136
 
126
137
  if (err) console.log(err)
127
138
 
128
- const track = utils.text.random(9)
129
139
  queue[track] = cb
130
- send(
131
- JSON.stringify({
132
- "#": dup.track(track),
133
- get: lex,
134
- }),
135
- )
140
+ send(request)
136
141
  // Respond to callback with null if no response.
137
142
  setTimeout(() => {
138
143
  const cb = queue[track]
@@ -143,14 +148,14 @@ const Wire = opt => {
143
148
  cb({put: ack})
144
149
  delete queue[track]
145
150
  }
146
- }, opt.wait || 100)
151
+ }, _opt.wait || 100)
147
152
  })
148
153
  }
149
154
 
150
155
  const api = send => {
151
156
  return {
152
- get: (lex, cb, opt) => {
153
- getWithCallback(lex, cb, send, opt)
157
+ get: (lex, cb, _opt) => {
158
+ getWithCallback(lex, cb, send, _opt)
154
159
  },
155
160
  put: async (data, cb) => {
156
161
  // Deferred updates are only stored using wire spec, they're ignored
@@ -172,7 +177,7 @@ const Wire = opt => {
172
177
  }),
173
178
  )
174
179
  },
175
- on: (lex, cb) => {
180
+ on: (lex, cb, _get, _opt) => {
176
181
  const soul = lex && lex["#"]
177
182
  if (!soul || !cb) return
178
183
 
@@ -181,6 +186,7 @@ const Wire = opt => {
181
186
  } else {
182
187
  listen[soul] = [{".": lex["."], cb: cb}]
183
188
  }
189
+ if (_get) getWithCallback(lex, cb, send, _opt)
184
190
  },
185
191
  off: (lex, cb) => {
186
192
  const soul = lex && lex["#"]
@@ -36,8 +36,31 @@ describe("holster.get", () => {
36
36
  })
37
37
  })
38
38
 
39
- // Chaining get creates a new context for each call.
40
- test("get chained unknown keys callback null", (t, done) => {
39
+ test("get unknown keys in for loop callbacks null", (t, done) => {
40
+ let count = 0
41
+ for (let i = 0; i < 5; i++) {
42
+ holster.get("unknown" + i, data => {
43
+ assert.equal(data, null)
44
+ count++
45
+ })
46
+ }
47
+ setTimeout(() => {
48
+ if (count === 5) done()
49
+ }, 200)
50
+ })
51
+
52
+ test("nested unknown keys both callbacks null", (t, done) => {
53
+ holster.get("unknown", data => {
54
+ assert.equal(data, null)
55
+
56
+ holster.get("unknown", data => {
57
+ assert.equal(data, null)
58
+ done()
59
+ })
60
+ })
61
+ })
62
+
63
+ test("get already called callback null", (t, done) => {
41
64
  holster.get("chained").get("unknown", data => {
42
65
  assert.equal(data, null)
43
66
  done()
@@ -51,6 +74,17 @@ describe("holster.get", () => {
51
74
  })
52
75
  })
53
76
 
77
+ test("nested next chained unknown keys callback null", (t, done) => {
78
+ holster.get("chained").next("unknown", data => {
79
+ assert.equal(data, null)
80
+
81
+ holster.get("chained").next("unknown", data => {
82
+ assert.equal(data, null)
83
+ done()
84
+ })
85
+ })
86
+ })
87
+
54
88
  test("cleanup", (t, done) => {
55
89
  fs.rm("test/holster.get", {recursive: true, force: true}, err => {
56
90
  assert.equal(err, null)
@@ -37,10 +37,12 @@ describe("holster.on", () => {
37
37
  done() // This done is not called and test would fail if it was.
38
38
  })
39
39
 
40
- holster.get("key2").put("value2", err => {
41
- assert.equal(err, null)
42
- done()
43
- })
40
+ setTimeout(() => {
41
+ holster.get("key2").put("value2", err => {
42
+ assert.equal(err, null)
43
+ done()
44
+ })
45
+ }, 100)
44
46
  })
45
47
 
46
48
  test("on for property on root two updates - two events", (t, done) => {
@@ -70,6 +72,45 @@ describe("holster.on", () => {
70
72
  })
71
73
  })
72
74
 
75
+ test("on for properties on root in for loop", (t, done) => {
76
+ for (let i = 0; i < 5; i++) {
77
+ holster.get("for" + i).put(i, err => {
78
+ assert.equal(err, null)
79
+ })
80
+ }
81
+
82
+ // Need to wait until after all initial puts are done to set on listeners.
83
+ setTimeout(() => {
84
+ for (let i = 0; i < 5; i++) {
85
+ holster.get("for" + i).on(data => {
86
+ assert.equal(data, "update" + i)
87
+ if (i === 4) done()
88
+ })
89
+ }
90
+
91
+ setTimeout(() => {
92
+ for (let i = 0; i < 5; i++) {
93
+ holster.get("for" + i).put("update" + i, err => {
94
+ assert.equal(err, null)
95
+ })
96
+ }
97
+ }, 100)
98
+ }, 500)
99
+ })
100
+
101
+ test("on with get flag set - data returned", (t, done) => {
102
+ holster.get("key4").put("value4", err => {
103
+ assert.equal(err, null)
104
+
105
+ setTimeout(() => {
106
+ holster.get("key4").on(data => {
107
+ assert.equal(data, "value4")
108
+ done()
109
+ }, true)
110
+ }, 100)
111
+ })
112
+ })
113
+
73
114
  test("chained next before on", (t, done) => {
74
115
  // The node needs to exist before it can be listend to for updates.
75
116
  holster
@@ -140,7 +181,7 @@ describe("holster.on", () => {
140
181
  },
141
182
  }
142
183
  holster.get("nested").on(data => {
143
- assert.deepEqual(data, update)
184
+ assert.deepEqual(data, {key: nested.key, ...update})
144
185
  })
145
186
 
146
187
  holster
@@ -63,6 +63,27 @@ describe("holster.put", () => {
63
63
  })
64
64
  })
65
65
 
66
+ test("put and get values in for loop on root", (t, done) => {
67
+ for (let i = 0; i < 5; i++) {
68
+ holster.get("for" + i).put(i, err => {
69
+ assert.equal(err, null)
70
+ })
71
+ }
72
+
73
+ setTimeout(() => {
74
+ let count = 0
75
+ for (let i = 0; i < 5; i++) {
76
+ holster.get("for" + i, data => {
77
+ assert.equal(data, i)
78
+ count++
79
+ })
80
+ }
81
+ setTimeout(() => {
82
+ if (count === 5) done()
83
+ }, 200)
84
+ }, 200)
85
+ })
86
+
66
87
  test("chained get before put", (t, done) => {
67
88
  holster
68
89
  .get("hello")
@@ -269,7 +290,7 @@ describe("holster.put", () => {
269
290
  })
270
291
  })
271
292
 
272
- test("holster put string with set", (t, done) => {
293
+ test("put string with set", (t, done) => {
273
294
  holster.get("set").put("first", true, err => {
274
295
  assert.equal(err, null)
275
296
 
@@ -292,7 +313,7 @@ describe("holster.put", () => {
292
313
  })
293
314
  })
294
315
 
295
- test("holster put object with set", (t, done) => {
316
+ test("put object with set", (t, done) => {
296
317
  const set1 = {
297
318
  key: "value 1",
298
319
  child: "child value 1",
@@ -43,14 +43,6 @@ describe("holster.secure", () => {
43
43
  })
44
44
  })
45
45
 
46
- // Chaining get creates a new context for each call.
47
- test("get chained unknown keys callback null", (t, done) => {
48
- holster.get("chained").get("unknown", data => {
49
- assert.equal(data, null)
50
- done()
51
- })
52
- })
53
-
54
46
  test("next chained unknown keys callback null", (t, done) => {
55
47
  holster.get("chained").next("unknown", data => {
56
48
  assert.equal(data, null)
@@ -102,7 +94,7 @@ describe("holster.secure", () => {
102
94
  })
103
95
  })
104
96
 
105
- test("put and get string", (t, done) => {
97
+ test("user put and get string", (t, done) => {
106
98
  user.get("key").put("value", err => {
107
99
  assert.equal(err, null)
108
100
 
@@ -62,11 +62,27 @@ describe("holster.user.get", () => {
62
62
  })
63
63
  })
64
64
 
65
- // Chaining get creates a new context for each call.
66
- test("get chained unknown keys callback null", (t, done) => {
67
- user.get("chained").get("unknown", data => {
65
+ test("get unknown keys in for loop callbacks null", (t, done) => {
66
+ let count = 0
67
+ for (let i = 0; i < 5; i++) {
68
+ user.get("unknown" + i, data => {
69
+ assert.equal(data, null)
70
+ count++
71
+ })
72
+ }
73
+ setTimeout(() => {
74
+ if (count === 5) done()
75
+ }, 200)
76
+ })
77
+
78
+ test("nested get unknown key callback null", (t, done) => {
79
+ user.get("unknown", data => {
68
80
  assert.equal(data, null)
69
- done()
81
+
82
+ user.get("unknown", data => {
83
+ assert.equal(data, null)
84
+ done()
85
+ })
70
86
  })
71
87
  })
72
88
 
@@ -77,6 +93,17 @@ describe("holster.user.get", () => {
77
93
  })
78
94
  })
79
95
 
96
+ test("nested next chained unknown keys callback null", (t, done) => {
97
+ user.get("chained").next("unknown", data => {
98
+ assert.equal(data, null)
99
+
100
+ user.get("chained").next("unknown", data => {
101
+ assert.equal(data, null)
102
+ done()
103
+ })
104
+ })
105
+ })
106
+
80
107
  test("cleanup", (t, done) => {
81
108
  fs.rm("test/holster.user.get", {recursive: true, force: true}, err => {
82
109
  assert.equal(err, null)
@@ -12,7 +12,6 @@ describe("holster.user.lex", () => {
12
12
  maxAge: 100,
13
13
  })
14
14
  const user = holster.user()
15
- let alice = ""
16
15
 
17
16
  test("user create", (t, done) => {
18
17
  user.create("alice", "password", err => {
@@ -25,7 +24,6 @@ describe("holster.user.lex", () => {
25
24
  user.auth("alice", "password", err => {
26
25
  assert.equal(err, null)
27
26
  assert.equal(user.is.username, "alice")
28
- alice = user.is.pub
29
27
  done()
30
28
  })
31
29
  })
@@ -84,6 +82,7 @@ describe("holster.user.lex", () => {
84
82
  other: "other child value",
85
83
  },
86
84
  }
85
+
87
86
  user.get("nested").put(nested, err => {
88
87
  assert.equal(err, null)
89
88
 
@@ -123,6 +122,7 @@ describe("holster.user.lex", () => {
123
122
  has: "child update",
124
123
  },
125
124
  }
125
+
126
126
  user
127
127
  .get("on-nested")
128
128
  .next("child")
@@ -69,6 +69,7 @@ describe("holster.user.on", () => {
69
69
  assert.equal(data, "update1")
70
70
  } else {
71
71
  assert.equal(data, "update2")
72
+ user.get("key3").off()
72
73
  done()
73
74
  }
74
75
  })
@@ -86,6 +87,58 @@ describe("holster.user.on", () => {
86
87
  })
87
88
  })
88
89
 
90
+ test("on for properties in for loop", (t, done) => {
91
+ ;(async () => {
92
+ for (let i = 0; i < 5; i++) {
93
+ // Need to be careful putting user properties in a loop as they are
94
+ // verified per node, so wait for each update to return.
95
+ const err = await new Promise(res => {
96
+ user.get("for" + i).put(i, res)
97
+ })
98
+ assert.equal(err, null)
99
+ }
100
+ })()
101
+
102
+ // Need to wait until after all initial puts are done to set on listeners.
103
+ setTimeout(async () => {
104
+ let count = 0
105
+ for (let i = 0; i < 5; i++) {
106
+ user.get("for" + i).on(data => {
107
+ assert.equal(data, "update" + i)
108
+ user.get("for" + i).off()
109
+ count++
110
+ })
111
+ }
112
+
113
+ setTimeout(async () => {
114
+ for (let i = 0; i < 5; i++) {
115
+ // Same as above put, wait for each update to return.
116
+ const err = await new Promise(res => {
117
+ user.get("for" + i).put("update" + i, res)
118
+ })
119
+ assert.equal(err, null)
120
+ }
121
+ setTimeout(() => {
122
+ if (count === 5) done()
123
+ }, 500)
124
+ }, 500)
125
+ }, 1000)
126
+ })
127
+
128
+ test("on with get flag set - data returned", (t, done) => {
129
+ user.get("key4").put("value4", err => {
130
+ assert.equal(err, null)
131
+
132
+ setTimeout(() => {
133
+ user.get("key4").on(data => {
134
+ assert.equal(data, "value4")
135
+ user.get("key4").off()
136
+ done()
137
+ }, true)
138
+ }, 100)
139
+ })
140
+ })
141
+
89
142
  test("chained next before on", (t, done) => {
90
143
  // The node needs to exist before it can be listend to for updates.
91
144
  user
@@ -119,6 +172,7 @@ describe("holster.user.on", () => {
119
172
  key: "plain value",
120
173
  number: 42,
121
174
  }
175
+
122
176
  user.get("plain").put(plain, err => {
123
177
  assert.equal(err, null)
124
178
 
@@ -126,6 +180,7 @@ describe("holster.user.on", () => {
126
180
  key: "update",
127
181
  number: 42,
128
182
  }
183
+
129
184
  user.get("plain").on(data => {
130
185
  assert.deepEqual(data, update)
131
186
  done()
@@ -156,6 +211,7 @@ describe("holster.user.on", () => {
156
211
  has: "child update",
157
212
  },
158
213
  }
214
+
159
215
  user.get("nested").on(data => {
160
216
  assert.deepEqual(data, update)
161
217
  })
@@ -177,9 +233,11 @@ describe("holster.user.on", () => {
177
233
  })
178
234
 
179
235
  test("cleanup", (t, done) => {
180
- fs.rm("test/holster.user.on", {recursive: true, force: true}, err => {
181
- assert.equal(err, null)
182
- done()
183
- })
236
+ setTimeout(() => {
237
+ fs.rm("test/holster.user.on", {recursive: true, force: true}, err => {
238
+ assert.equal(err, null)
239
+ done()
240
+ })
241
+ }, 100)
184
242
  })
185
243
  })