@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.
- package/.github/workflows/node.js.yml +31 -0
- package/LICENSE.md +55 -0
- package/README.md +24 -0
- package/build/holster.js +4 -0
- package/build/holster.js.map +7 -0
- package/examples/index.html +33 -0
- package/examples/index.js +49 -0
- package/examples/user.html +31 -0
- package/examples/user.js +88 -0
- package/examples/wire.html +27 -0
- package/examples/wire.js +55 -0
- package/package.json +44 -0
- package/src/array.js +34 -0
- package/src/buffer.js +83 -0
- package/src/dup.js +23 -0
- package/src/get.js +19 -0
- package/src/ham.js +151 -0
- package/src/holster.js +502 -0
- package/src/index.js +21 -0
- package/src/radisk.js +438 -0
- package/src/radix.js +106 -0
- package/src/sea-utils.js +65 -0
- package/src/sea.js +224 -0
- package/src/store.js +225 -0
- package/src/user.js +330 -0
- package/src/utils.js +124 -0
- package/src/wire.js +290 -0
- package/test/holster.get.test.js +60 -0
- package/test/holster.lex.test.js +196 -0
- package/test/holster.off.test.js +53 -0
- package/test/holster.on.test.js +168 -0
- package/test/holster.put.test.js +278 -0
- package/test/holster.secure.test.js +210 -0
- package/test/holster.user.get.test.js +86 -0
- package/test/holster.user.lex.test.js +148 -0
- package/test/holster.user.off.test.js +72 -0
- package/test/holster.user.on.test.js +185 -0
- package/test/holster.user.put.test.js +381 -0
- package/test/kitty/! +9 -0
- package/test/names.js +225 -0
- package/test/radisk.test.js +191 -0
- package/test/radix.test.js +117 -0
- package/test/sea.test.js +284 -0
- package/test/store.test.js +316 -0
- package/test/user.test.js +173 -0
- 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
|
+
}
|