@mblaney/holster 0.2.0 → 1.0.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/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,27 @@ 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
+ for (let i = 0; i < 5; i++) {
41
+ holster.get("unknown" + i, data => {
42
+ assert.equal(data, null)
43
+ if (i === 4) done()
44
+ })
45
+ }
46
+ })
47
+
48
+ test("nested unknown keys both callbacks null", (t, done) => {
49
+ holster.get("unknown", data => {
50
+ assert.equal(data, null)
51
+
52
+ holster.get("unknown", data => {
53
+ assert.equal(data, null)
54
+ done()
55
+ })
56
+ })
57
+ })
58
+
59
+ test("get already called callback null", (t, done) => {
41
60
  holster.get("chained").get("unknown", data => {
42
61
  assert.equal(data, null)
43
62
  done()
@@ -51,6 +70,17 @@ describe("holster.get", () => {
51
70
  })
52
71
  })
53
72
 
73
+ test("nested next chained unknown keys callback null", (t, done) => {
74
+ holster.get("chained").next("unknown", data => {
75
+ assert.equal(data, null)
76
+
77
+ holster.get("chained").next("unknown", data => {
78
+ assert.equal(data, null)
79
+ done()
80
+ })
81
+ })
82
+ })
83
+
54
84
  test("cleanup", (t, done) => {
55
85
  fs.rm("test/holster.get", {recursive: true, force: true}, err => {
56
86
  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,23 @@ 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
+ for (let i = 0; i < 5; i++) {
75
+ holster.get("for" + i, data => {
76
+ assert.equal(data, i)
77
+ if (i === 4) setTimeout(done, 100)
78
+ })
79
+ }
80
+ }, 200)
81
+ })
82
+
66
83
  test("chained get before put", (t, done) => {
67
84
  holster
68
85
  .get("hello")
@@ -269,7 +286,7 @@ describe("holster.put", () => {
269
286
  })
270
287
  })
271
288
 
272
- test("holster put string with set", (t, done) => {
289
+ test("put string with set", (t, done) => {
273
290
  holster.get("set").put("first", true, err => {
274
291
  assert.equal(err, null)
275
292
 
@@ -292,7 +309,7 @@ describe("holster.put", () => {
292
309
  })
293
310
  })
294
311
 
295
- test("holster put object with set", (t, done) => {
312
+ test("put object with set", (t, done) => {
296
313
  const set1 = {
297
314
  key: "value 1",
298
315
  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,23 @@ 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
+ for (let i = 0; i < 5; i++) {
67
+ user.get("unknown" + i, data => {
68
+ assert.equal(data, null)
69
+ if (i === 4) done()
70
+ })
71
+ }
72
+ })
73
+
74
+ test("nested get unknown key callback null", (t, done) => {
75
+ user.get("unknown", data => {
68
76
  assert.equal(data, null)
69
- done()
77
+
78
+ user.get("unknown", data => {
79
+ assert.equal(data, null)
80
+ done()
81
+ })
70
82
  })
71
83
  })
72
84
 
@@ -77,6 +89,17 @@ describe("holster.user.get", () => {
77
89
  })
78
90
  })
79
91
 
92
+ test("nested next chained unknown keys callback null", (t, done) => {
93
+ user.get("chained").next("unknown", data => {
94
+ assert.equal(data, null)
95
+
96
+ user.get("chained").next("unknown", data => {
97
+ assert.equal(data, null)
98
+ done()
99
+ })
100
+ })
101
+ })
102
+
80
103
  test("cleanup", (t, done) => {
81
104
  fs.rm("test/holster.user.get", {recursive: true, force: true}, err => {
82
105
  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,54 @@ 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
+ for (let i = 0; i < 5; i++) {
105
+ user.get("for" + i).on(data => {
106
+ assert.equal(data, "update" + i)
107
+ user.get("for" + i).off()
108
+ if (i === 4) done()
109
+ })
110
+ }
111
+
112
+ setTimeout(async () => {
113
+ for (let i = 0; i < 5; i++) {
114
+ // Same as above put, wait for each update to return.
115
+ const err = await new Promise(res => {
116
+ user.get("for" + i).put("update" + i, res)
117
+ })
118
+ assert.equal(err, null)
119
+ }
120
+ }, 500)
121
+ }, 1000)
122
+ })
123
+
124
+ test("on with get flag set - data returned", (t, done) => {
125
+ user.get("key4").put("value4", err => {
126
+ assert.equal(err, null)
127
+
128
+ setTimeout(() => {
129
+ user.get("key4").on(data => {
130
+ assert.equal(data, "value4")
131
+ user.get("key4").off()
132
+ done()
133
+ }, true)
134
+ }, 100)
135
+ })
136
+ })
137
+
89
138
  test("chained next before on", (t, done) => {
90
139
  // The node needs to exist before it can be listend to for updates.
91
140
  user
@@ -119,6 +168,7 @@ describe("holster.user.on", () => {
119
168
  key: "plain value",
120
169
  number: 42,
121
170
  }
171
+
122
172
  user.get("plain").put(plain, err => {
123
173
  assert.equal(err, null)
124
174
 
@@ -126,6 +176,7 @@ describe("holster.user.on", () => {
126
176
  key: "update",
127
177
  number: 42,
128
178
  }
179
+
129
180
  user.get("plain").on(data => {
130
181
  assert.deepEqual(data, update)
131
182
  done()
@@ -156,6 +207,7 @@ describe("holster.user.on", () => {
156
207
  has: "child update",
157
208
  },
158
209
  }
210
+
159
211
  user.get("nested").on(data => {
160
212
  assert.deepEqual(data, update)
161
213
  })
@@ -177,9 +229,11 @@ describe("holster.user.on", () => {
177
229
  })
178
230
 
179
231
  test("cleanup", (t, done) => {
180
- fs.rm("test/holster.user.on", {recursive: true, force: true}, err => {
181
- assert.equal(err, null)
182
- done()
183
- })
232
+ setTimeout(() => {
233
+ fs.rm("test/holster.user.on", {recursive: true, force: true}, err => {
234
+ assert.equal(err, null)
235
+ done()
236
+ })
237
+ }, 100)
184
238
  })
185
239
  })