@mblaney/holster 0.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.
Files changed (46) hide show
  1. package/.github/workflows/node.js.yml +31 -0
  2. package/LICENSE.md +55 -0
  3. package/README.md +24 -0
  4. package/build/holster.js +4 -0
  5. package/build/holster.js.map +7 -0
  6. package/examples/index.html +33 -0
  7. package/examples/index.js +49 -0
  8. package/examples/user.html +31 -0
  9. package/examples/user.js +88 -0
  10. package/examples/wire.html +27 -0
  11. package/examples/wire.js +55 -0
  12. package/package.json +44 -0
  13. package/src/array.js +34 -0
  14. package/src/buffer.js +83 -0
  15. package/src/dup.js +23 -0
  16. package/src/get.js +19 -0
  17. package/src/ham.js +151 -0
  18. package/src/holster.js +502 -0
  19. package/src/index.js +21 -0
  20. package/src/radisk.js +438 -0
  21. package/src/radix.js +106 -0
  22. package/src/sea-utils.js +65 -0
  23. package/src/sea.js +224 -0
  24. package/src/store.js +225 -0
  25. package/src/user.js +330 -0
  26. package/src/utils.js +124 -0
  27. package/src/wire.js +290 -0
  28. package/test/holster.get.test.js +60 -0
  29. package/test/holster.lex.test.js +196 -0
  30. package/test/holster.off.test.js +53 -0
  31. package/test/holster.on.test.js +168 -0
  32. package/test/holster.put.test.js +278 -0
  33. package/test/holster.secure.test.js +210 -0
  34. package/test/holster.user.get.test.js +86 -0
  35. package/test/holster.user.lex.test.js +148 -0
  36. package/test/holster.user.off.test.js +72 -0
  37. package/test/holster.user.on.test.js +185 -0
  38. package/test/holster.user.put.test.js +381 -0
  39. package/test/kitty/! +9 -0
  40. package/test/names.js +225 -0
  41. package/test/radisk.test.js +191 -0
  42. package/test/radix.test.js +117 -0
  43. package/test/sea.test.js +284 -0
  44. package/test/store.test.js +316 -0
  45. package/test/user.test.js +173 -0
  46. package/test/wire.test.js +159 -0
package/src/user.js ADDED
@@ -0,0 +1,330 @@
1
+ import * as utils from "./utils.js"
2
+ import Wire from "./wire.js"
3
+ import SEA from "./sea.js"
4
+
5
+ const User = (opt, wire) => {
6
+ if (!wire) wire = Wire(opt)
7
+ let pubs = []
8
+ let creating = false
9
+ let authing = false
10
+ let retries = 0
11
+
12
+ const auth = (username, password, newPassword, ack) => {
13
+ const retry = username => {
14
+ retries++
15
+ auth(username, password, newPassword, ack)
16
+ }
17
+ const done = err => {
18
+ pubs = []
19
+ retries = 0
20
+ authing = false
21
+ ack(err)
22
+ }
23
+ const next = () => {
24
+ if (pubs.length === 0) {
25
+ done("Wrong username or password")
26
+ return
27
+ }
28
+
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
+ }
35
+
36
+ const data = msg.put && msg.put[pub]
37
+ if (!data || !data.auth) return next()
38
+
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()
43
+
44
+ user.is = {
45
+ username: username,
46
+ pub: data.pub,
47
+ epub: data.epub,
48
+ priv: dec.priv,
49
+ epriv: dec.epriv,
50
+ }
51
+ // Holster API uses ctx to set the root node for user data.
52
+ user.ctx = "~" + data.pub
53
+
54
+ if (newPassword !== "") {
55
+ // Encrypt private key using new password and a new salt.
56
+ const salt = utils.text.random(64)
57
+ const work = await SEA.work(newPassword, salt)
58
+ 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)
61
+ const graph = utils.graph(pub, signed.m, signed.s, data.pub)
62
+ wire.put(graph, err => {
63
+ if (err) {
64
+ done(`error putting ${auth} on ${pub}: ${err}`)
65
+ } else {
66
+ done(null)
67
+ }
68
+ })
69
+ return
70
+ }
71
+
72
+ return done(null)
73
+ })
74
+ }
75
+
76
+ if (retries > 9) {
77
+ done("Wrong username or password")
78
+ return
79
+ }
80
+
81
+ const soul = "~@" + username
82
+ wire.get({"#": soul}, async msg => {
83
+ if (msg.err) {
84
+ done(`error getting ${soul}: ${msg.err}`)
85
+ return
86
+ }
87
+
88
+ const data = msg.put && msg.put[soul]
89
+ if (!data) return retry(username)
90
+
91
+ delete msg.put[soul]._
92
+ // Usernames aren't guaranteed to be unique, so store the list and then
93
+ // try each of them until one is successful with the given password.
94
+ pubs = Object.keys(data)
95
+ next()
96
+ })
97
+ }
98
+
99
+ const user = {
100
+ create: (username, password, cb) => {
101
+ const ack = err => {
102
+ if (cb) cb(err)
103
+ else console.log(err)
104
+ }
105
+
106
+ if (creating) {
107
+ ack("User is already being created")
108
+ return
109
+ }
110
+
111
+ if (username === "") {
112
+ ack("Please provide a username")
113
+ return
114
+ }
115
+
116
+ if (password === "") {
117
+ ack("Please provide a password")
118
+ return
119
+ }
120
+
121
+ creating = true
122
+
123
+ const soul = "~@" + username
124
+ wire.get({"#": soul}, async msg => {
125
+ if (msg.err) {
126
+ creating = false
127
+ ack(`error getting ${soul}: ${msg.err}`)
128
+ return
129
+ }
130
+
131
+ if (msg.put && msg.put[soul]) {
132
+ creating = false
133
+ ack("Username already exists")
134
+ return
135
+ }
136
+
137
+ const salt = utils.text.random(64)
138
+ const work = await SEA.work(password, salt)
139
+ const pair = await SEA.pair()
140
+ const priv = {priv: pair.priv, epriv: pair.epriv}
141
+ const enc = await SEA.encrypt(priv, work)
142
+ const data = {
143
+ username: username,
144
+ pub: pair.pub,
145
+ epub: pair.epub,
146
+ auth: JSON.stringify({enc: enc, salt: salt}),
147
+ }
148
+
149
+ const pub = "~" + pair.pub
150
+ const signed = await SEA.sign(data, pair)
151
+ const graph = utils.graph(pub, signed.m, signed.s, pair.pub)
152
+ wire.put(graph, err => {
153
+ creating = false
154
+ if (err) {
155
+ ack(`error putting ${data} on ${pub}: ${err}`)
156
+ return
157
+ }
158
+
159
+ const rel = {[pub]: {"#": pub}}
160
+ wire.put(utils.graph(soul, rel), err => {
161
+ if (err) {
162
+ ack(`error putting ${rel} on ${soul}: ${err}`)
163
+ return
164
+ }
165
+
166
+ // Return null on success.
167
+ if (cb) cb(null)
168
+ })
169
+ })
170
+ })
171
+ },
172
+ auth: (username, password, cb) => {
173
+ const ack = err => {
174
+ if (cb) cb(err)
175
+ else if (err) console.log(err)
176
+ }
177
+
178
+ if (authing) {
179
+ ack("User is already authenticating")
180
+ return
181
+ }
182
+
183
+ user.is = null
184
+
185
+ if (username === "") {
186
+ ack("Please provide a username")
187
+ return
188
+ }
189
+
190
+ if (password === "") {
191
+ ack("Please provide a password")
192
+ return
193
+ }
194
+
195
+ authing = true
196
+ auth(username, password, "", ack)
197
+ },
198
+ change: (username, password, newPassword, cb) => {
199
+ const ack = err => {
200
+ if (cb) cb(err)
201
+ else if (err) console.log(err)
202
+ }
203
+
204
+ if (authing) {
205
+ ack("User is already authenticating")
206
+ return
207
+ }
208
+
209
+ user.is = null
210
+
211
+ if (username === "") {
212
+ ack("Please provide a username")
213
+ return
214
+ }
215
+
216
+ if (password === "") {
217
+ ack("Please provide a password")
218
+ return
219
+ }
220
+
221
+ if (newPassword === "") {
222
+ ack("Please provide a new password")
223
+ return
224
+ }
225
+
226
+ authing = true
227
+ auth(username, password, newPassword, ack)
228
+ },
229
+ store: localStorage => {
230
+ if (!user.is) {
231
+ console.log("Please authenticate before calling store")
232
+ return
233
+ }
234
+
235
+ if (localStorage) {
236
+ if (typeof globalThis.localStorage !== "undefined") {
237
+ globalThis.localStorage.setItem("user.is", JSON.stringify(user.is))
238
+ }
239
+ return
240
+ }
241
+
242
+ if (typeof globalThis.sessionStorage !== "undefined") {
243
+ globalThis.sessionStorage.setItem("user.is", JSON.stringify(user.is))
244
+ }
245
+ },
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
+ }
256
+ }
257
+ return
258
+ }
259
+
260
+ if (typeof globalThis.sessionStorage !== "undefined") {
261
+ const is = globalThis.sessionStorage.getItem("user.is")
262
+ if (is) {
263
+ 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
+ }
269
+ },
270
+ leave: () => {
271
+ user.is = null
272
+ user.ctx = null
273
+ if (typeof globalThis.localStorage !== "undefined") {
274
+ globalThis.localStorage.removeItem("user.is")
275
+ }
276
+ if (typeof globalThis.sessionStorage !== "undefined") {
277
+ globalThis.sessionStorage.removeItem("user.is")
278
+ }
279
+ },
280
+ delete: (username, password, cb) => {
281
+ const ack = err => {
282
+ if (cb) cb(err)
283
+ else console.log(err)
284
+ }
285
+
286
+ if (authing) {
287
+ ack("User is already authenticating")
288
+ return
289
+ }
290
+
291
+ if (username === "") {
292
+ ack("Please provide a username")
293
+ return
294
+ }
295
+
296
+ if (password === "") {
297
+ ack("Please provide a password")
298
+ return
299
+ }
300
+
301
+ authing = true
302
+ auth(username, password, "", async err => {
303
+ if (err) {
304
+ ack(err)
305
+ return
306
+ }
307
+
308
+ const data = {username: null, pub: null, epub: null, auth: null}
309
+ const signed = await SEA.sign(data, user.is)
310
+ const pub = "~" + user.is.pub
311
+ const graph = utils.graph(pub, signed.m, signed.s, user.is.pub)
312
+ wire.put(graph, err => {
313
+ if (err) {
314
+ ack(`error putting null on ${pub}: ${err}`)
315
+ return
316
+ }
317
+
318
+ user.is = null
319
+ user.ctx = null
320
+ // Return null on success. Note currently not updating ~@username,
321
+ // not sure if allowing username re-user is a good idea anyway?
322
+ if (cb) cb(null)
323
+ })
324
+ })
325
+ },
326
+ }
327
+ return user
328
+ }
329
+
330
+ export default User
package/src/utils.js ADDED
@@ -0,0 +1,124 @@
1
+ export const num = {
2
+ is: n =>
3
+ !(n instanceof Array) &&
4
+ (n - parseFloat(n) + 1 >= 0 || Infinity === n || -Infinity === n),
5
+ }
6
+
7
+ export const obj = {
8
+ is: o => {
9
+ if (!o) return false
10
+
11
+ return (
12
+ (o instanceof Object && o.constructor === Object) ||
13
+ Object.prototype.toString.call(o).match(/^\[object (\w+)\]$/)[1] ===
14
+ "Object"
15
+ )
16
+ },
17
+ map: (list, cb, o) => {
18
+ var keys = Object.keys(list)
19
+ for (let i = 0; i < keys.length; i++) {
20
+ let result = cb(list[keys[i]], keys[i], o)
21
+ if (typeof result !== "undefined") return result
22
+ }
23
+ },
24
+ put: (o, key, value) => {
25
+ if (!o) o = {}
26
+ o[key] = value
27
+ return o
28
+ },
29
+ del: (o, key) => {
30
+ if (!o) return
31
+
32
+ o[key] = null
33
+ delete o[key]
34
+ return o
35
+ },
36
+ }
37
+
38
+ const map_soul = (soul, key, o) => {
39
+ // If id is already defined AND we're still looping through the object,
40
+ // then it is considered invalid.
41
+ if (o.id) {
42
+ o.id = false
43
+ return
44
+ }
45
+
46
+ if (key === "#" && typeof soul === "string") {
47
+ o.id = soul
48
+ return
49
+ }
50
+
51
+ // If there exists anything else on the object that isn't the soul,
52
+ // then it is considered invalid.
53
+ o.id = false
54
+ }
55
+
56
+ // Check if an object is a soul relation, ie {'#': 'UUID'}
57
+ export const rel = {
58
+ is: value => {
59
+ if (value && value["#"] && !value._ && obj.is(value)) {
60
+ let o = {}
61
+ obj.map(value, map_soul, o)
62
+ if (o.id) return o.id
63
+ }
64
+
65
+ return false
66
+ },
67
+ // Convert a soul into a relation and return it.
68
+ ify: soul => obj.put({}, "#", soul),
69
+ }
70
+
71
+ export const userPublicKey = "_holster_user_public_key"
72
+
73
+ // graph converts objects to graph format with updated states,
74
+ // with optional meta data to verify signed data.
75
+ export const graph = (soul, data, sig, pub) => {
76
+ const g = {[soul]: {_: {"#": soul, ">": {}, s: sig, p: pub}}}
77
+ for (const [key, value] of Object.entries(data)) {
78
+ g[soul][key] = value
79
+ g[soul]._[">"][key] = Date.now()
80
+ }
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
+ g[soul][userPublicKey] = pub
85
+ g[soul]._[">"][userPublicKey] = Date.now()
86
+ }
87
+ return g
88
+ }
89
+
90
+ export const match = (lex, key) => {
91
+ // Null is used to match listeners on souls, which don't provide a key.
92
+ if (typeof key === "undefined") return lex === null
93
+
94
+ if (typeof lex === "undefined") return true
95
+
96
+ if (typeof lex === "string") return lex === key
97
+
98
+ if (!obj.is(lex) || !key) return false
99
+
100
+ const prefix = lex["*"]
101
+ if (prefix) return key.slice(0, prefix.length) === prefix
102
+
103
+ const gt = lex[">"]
104
+ const lt = lex["<"]
105
+ if (gt && lt) return key >= gt && key <= lt
106
+
107
+ if (gt) return key >= gt
108
+
109
+ if (lt) return key <= lt
110
+
111
+ return false
112
+ }
113
+
114
+ export const text = {
115
+ random: length => {
116
+ var s = ""
117
+ const c = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz"
118
+ if (!length) length = 24
119
+ for (let i = 0; i < length; i++) {
120
+ s += c.charAt(Math.floor(Math.random() * c.length))
121
+ }
122
+ return s
123
+ },
124
+ }