@mblaney/holster 1.0.35 → 1.0.36
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/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/dist/holster.js +4 -0
- package/dist/holster.js.map +7 -0
- package/package.json +23 -5
- package/src/get.js +6 -18
- package/src/ham.js +17 -47
- package/src/holster.js +106 -28
- package/src/sea.js +0 -52
- package/src/store.js +7 -7
- package/src/wire.js +7 -1
- package/ts/src/array.ts +78 -0
- package/ts/src/buffer.ts +441 -0
- package/ts/src/dup.ts +49 -0
- package/ts/src/get.ts +83 -0
- package/ts/src/ham.ts +254 -0
- package/ts/src/holster.ts +931 -0
- package/ts/src/index.ts +35 -0
- package/ts/src/radisk.ts +585 -0
- package/ts/src/radix.ts +157 -0
- package/ts/src/schemas.ts +519 -0
- package/ts/src/sea-utils.ts +103 -0
- package/ts/src/sea.ts +388 -0
- package/ts/src/store.ts +321 -0
- package/ts/src/user.ts +381 -0
- package/ts/src/utils.ts +232 -0
- package/ts/src/wire.ts +829 -0
- package/.dockerignore +0 -9
- package/.github/workflows/node.js.yml +0 -31
- package/.github/workflows/npm-publish.yml +0 -33
- package/Dockerfile +0 -20
- package/examples/index.html +0 -33
- package/examples/index.js +0 -49
- package/examples/user.html +0 -31
- package/examples/user.js +0 -93
- package/examples/wire.html +0 -27
- package/examples/wire.js +0 -55
- package/test/holster.get.test.js +0 -94
- package/test/holster.lex.test.js +0 -196
- package/test/holster.off.test.js +0 -53
- package/test/holster.on.test.js +0 -264
- package/test/holster.put.test.js +0 -358
- package/test/holster.secure.test.js +0 -202
- package/test/holster.user.get.test.js +0 -114
- package/test/holster.user.lex.test.js +0 -149
- package/test/holster.user.off.test.js +0 -73
- package/test/holster.user.on.test.js +0 -300
- package/test/holster.user.put.test.js +0 -463
- package/test/kitty/! +0 -9
- package/test/names.js +0 -225
- package/test/radisk.cache.test.js +0 -213
- package/test/radisk.test.js +0 -331
- package/test/radix.test.js +0 -117
- package/test/sea.test.js +0 -284
- package/test/split.test.js +0 -317
- package/test/store.test.js +0 -316
- package/test/system/basic.test.js +0 -29
- package/test/system/chained-on.test.js +0 -41
- package/test/system/concurrent-listeners.test.js +0 -125
- package/test/system/immediate-read.test.js +0 -30
- package/test/system/listener-before-data.test.js +0 -51
- package/test/system/promise-wrapper.test.js +0 -47
- package/test/system/user-chained-on.test.js +0 -60
- package/test/system/user-concurrent-listeners.test.js +0 -142
- package/test/system/user-listener-before-data.test.js +0 -66
- package/test/user.test.js +0 -173
- package/test/wire.test.js +0 -158
package/ts/src/store.ts
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store - Storage interface for graph data
|
|
3
|
+
* Provides get and put methods with filesystem or IndexedDB backend
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Radisk from "./radisk.ts"
|
|
7
|
+
import Radix from "./radix.ts"
|
|
8
|
+
import * as utils from "./utils.ts"
|
|
9
|
+
import type {
|
|
10
|
+
Lex,
|
|
11
|
+
Graph,
|
|
12
|
+
GraphNode,
|
|
13
|
+
StoreOptions,
|
|
14
|
+
FileSystemInterface,
|
|
15
|
+
EncodedValue,
|
|
16
|
+
} from "./schemas.ts"
|
|
17
|
+
|
|
18
|
+
const isNode = typeof document === "undefined"
|
|
19
|
+
|
|
20
|
+
// Dynamic import for Node.js fs module - won't execute in browser/service worker
|
|
21
|
+
const fs = isNode
|
|
22
|
+
? await import(/*webpackIgnore: true*/ "node:fs")
|
|
23
|
+
: undefined
|
|
24
|
+
|
|
25
|
+
// ASCII character for enquiry
|
|
26
|
+
const enq = String.fromCharCode(5)
|
|
27
|
+
// ASCII character for unit separator
|
|
28
|
+
const unit = String.fromCharCode(31)
|
|
29
|
+
// On-disk root node format
|
|
30
|
+
const root = unit + "+0" + unit + "#" + unit + '"root' + unit
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create file system storage backend
|
|
34
|
+
*/
|
|
35
|
+
const fileSystem = (opt: StoreOptions): FileSystemInterface => {
|
|
36
|
+
const dir = opt.file!
|
|
37
|
+
|
|
38
|
+
if (isNode) {
|
|
39
|
+
if (!fs!.existsSync(dir)) fs!.mkdirSync(dir)
|
|
40
|
+
if (!fs!.existsSync(dir + "/!")) fs!.writeFileSync(dir + "/!", root)
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
get: (file, cb) => {
|
|
44
|
+
fs!.readFile(dir + "/" + file, (err: NodeJS.ErrnoException | null, data?: Buffer) => {
|
|
45
|
+
if (err) {
|
|
46
|
+
if ((err as { code?: string }).code === "ENOENT") {
|
|
47
|
+
cb()
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
console.log("fs.readFile error:", err)
|
|
51
|
+
}
|
|
52
|
+
const dataStr = data ? data.toString() : undefined
|
|
53
|
+
cb(err as never, dataStr)
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
put: (file, data, cb) => {
|
|
57
|
+
const tmp = file + "." + utils.text.random(9) + ".tmp"
|
|
58
|
+
fs!.writeFile(tmp, data as string, (err: NodeJS.ErrnoException | null) => {
|
|
59
|
+
if (err) {
|
|
60
|
+
console.log("fs.writeFile error:", err)
|
|
61
|
+
cb(err as never)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
fs!.rename(tmp, dir + "/" + file, cb as never)
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
list: cb => {
|
|
68
|
+
fs!.readdir(dir, (err: NodeJS.ErrnoException | null, files?: string[]) => {
|
|
69
|
+
if (err) {
|
|
70
|
+
console.log("fs.readdir error:", err)
|
|
71
|
+
cb()
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
if (files) files.forEach(cb)
|
|
75
|
+
cb()
|
|
76
|
+
})
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (opt.indexedDB) {
|
|
82
|
+
let db: IDBDatabase
|
|
83
|
+
const dbReady = new Promise<void>((resolve, reject) => {
|
|
84
|
+
const o = indexedDB.open(dir, 1)
|
|
85
|
+
o.onupgradeneeded = event => {
|
|
86
|
+
;(event.target as IDBOpenDBRequest).result.createObjectStore(dir)
|
|
87
|
+
}
|
|
88
|
+
o.onerror = event => {
|
|
89
|
+
console.log(event)
|
|
90
|
+
reject(event)
|
|
91
|
+
}
|
|
92
|
+
o.onsuccess = () => {
|
|
93
|
+
db = o.result
|
|
94
|
+
if (db) {
|
|
95
|
+
const tx = db.transaction([dir], "readonly")
|
|
96
|
+
const req = tx.objectStore(dir).getKey("!")
|
|
97
|
+
req.onerror = () => {
|
|
98
|
+
console.log(`error getting key ${dir}/!`)
|
|
99
|
+
reject(req.error)
|
|
100
|
+
}
|
|
101
|
+
req.onsuccess = () => {
|
|
102
|
+
if (!req.result) {
|
|
103
|
+
const tx = db.transaction([dir], "readwrite")
|
|
104
|
+
const req = tx.objectStore(dir).put(root, "!")
|
|
105
|
+
req.onerror = () => {
|
|
106
|
+
console.log(`error putting root on ${dir}/!`)
|
|
107
|
+
reject(req.error)
|
|
108
|
+
}
|
|
109
|
+
req.onsuccess = () => {
|
|
110
|
+
resolve()
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
resolve()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
console.log("error indexedDB not available")
|
|
118
|
+
reject(new Error("indexedDB not available"))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
get: (file, cb) => {
|
|
125
|
+
const _get = (file: string, cb: (err?: string | null, data?: string) => void) => {
|
|
126
|
+
const tx = db.transaction([dir], "readonly")
|
|
127
|
+
const req = tx.objectStore(dir).get(file)
|
|
128
|
+
req.onerror = () => {
|
|
129
|
+
console.log(`error getting ${dir}/${file}`)
|
|
130
|
+
cb(req.error as unknown as string)
|
|
131
|
+
}
|
|
132
|
+
req.onsuccess = () => {
|
|
133
|
+
cb(null, req.result)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
dbReady
|
|
138
|
+
.then(() => {
|
|
139
|
+
_get(file, cb)
|
|
140
|
+
})
|
|
141
|
+
.catch(err => {
|
|
142
|
+
cb(err as string)
|
|
143
|
+
})
|
|
144
|
+
},
|
|
145
|
+
put: (file, data, cb) => {
|
|
146
|
+
const _put = (
|
|
147
|
+
file: string,
|
|
148
|
+
data: string,
|
|
149
|
+
cb: (err?: string | null) => void
|
|
150
|
+
) => {
|
|
151
|
+
const tx = db.transaction([dir], "readwrite")
|
|
152
|
+
const req = tx.objectStore(dir).put(data, file)
|
|
153
|
+
req.onerror = () => {
|
|
154
|
+
console.log(`error putting data on ${dir}/${file}`)
|
|
155
|
+
cb(req.error as unknown as string)
|
|
156
|
+
}
|
|
157
|
+
req.onsuccess = () => {
|
|
158
|
+
cb(null)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
dbReady
|
|
163
|
+
.then(() => {
|
|
164
|
+
_put(file, data as string, cb)
|
|
165
|
+
})
|
|
166
|
+
.catch(err => {
|
|
167
|
+
cb(err as string)
|
|
168
|
+
})
|
|
169
|
+
},
|
|
170
|
+
list: cb => {
|
|
171
|
+
const _list = (cb: (file?: string) => void) => {
|
|
172
|
+
const tx = db.transaction([dir], "readonly")
|
|
173
|
+
const req = tx.objectStore(dir).getAllKeys()
|
|
174
|
+
req.onerror = () => {
|
|
175
|
+
console.log("error getting keys for", dir)
|
|
176
|
+
cb()
|
|
177
|
+
}
|
|
178
|
+
req.onsuccess = () => {
|
|
179
|
+
;(req.result as string[]).forEach(cb)
|
|
180
|
+
cb()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
dbReady
|
|
185
|
+
.then(() => {
|
|
186
|
+
_list(cb)
|
|
187
|
+
})
|
|
188
|
+
.catch(err => {
|
|
189
|
+
console.log("error indexedDB not available:", err)
|
|
190
|
+
cb()
|
|
191
|
+
})
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// No browser storage
|
|
197
|
+
return {
|
|
198
|
+
get: (_file, cb) => {
|
|
199
|
+
cb(null, root)
|
|
200
|
+
},
|
|
201
|
+
put: (_file, _data, cb) => {
|
|
202
|
+
cb(null)
|
|
203
|
+
},
|
|
204
|
+
list: cb => {
|
|
205
|
+
cb("!")
|
|
206
|
+
cb()
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface StoreInterface {
|
|
212
|
+
get: (
|
|
213
|
+
lex: Lex,
|
|
214
|
+
cb: (err?: string | null, graph?: Graph) => void,
|
|
215
|
+
_opt?: { secure?: boolean }
|
|
216
|
+
) => void
|
|
217
|
+
put: (graph: Graph, cb: (err?: string | null) => void) => void
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Create Store with get and put methods
|
|
222
|
+
*/
|
|
223
|
+
const Store = (opt?: StoreOptions): StoreInterface => {
|
|
224
|
+
const options = opt || {}
|
|
225
|
+
options.file = String(options.file || "radata")
|
|
226
|
+
if (!options.store) options.store = fileSystem(options)
|
|
227
|
+
const radisk = Radisk(options as never)
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
get: (lex, cb, _opt) => {
|
|
231
|
+
if (!lex || !utils.obj.is(lex)) {
|
|
232
|
+
cb("lex required")
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
if (!lex["#"]) {
|
|
236
|
+
cb("soul required in lex")
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const opts = _opt || {}
|
|
241
|
+
const soul = lex["#"]
|
|
242
|
+
const key =
|
|
243
|
+
!opts.secure && typeof lex["."] === "string" ? lex["."] : ""
|
|
244
|
+
let node: GraphNode | undefined
|
|
245
|
+
const signatures: Record<string, string> = {}
|
|
246
|
+
|
|
247
|
+
const each = (value: EncodedValue, key: string): void => {
|
|
248
|
+
// Always include userPublicKey for verification, regardless of filter
|
|
249
|
+
if (key !== utils.userPublicKey && !utils.match(lex["."], key)) return
|
|
250
|
+
|
|
251
|
+
if (!node) node = { _: { "#": soul, ">": {} } }
|
|
252
|
+
node[key] = value[0]
|
|
253
|
+
node._[">"][key] = value[1]
|
|
254
|
+
// If signature is present, store it in _["s"]
|
|
255
|
+
if (value.length === 3 && value[2]) {
|
|
256
|
+
signatures[value[1].toString()] = value[2]
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
radisk(soul + enq + key, (err, value) => {
|
|
261
|
+
let graph: Graph | undefined
|
|
262
|
+
if (utils.obj.is(value)) {
|
|
263
|
+
Radix.map(value as never, each)
|
|
264
|
+
if (!node) each(value as never, key)
|
|
265
|
+
graph = { [soul]: node! }
|
|
266
|
+
} else if (value) {
|
|
267
|
+
each(value as EncodedValue, key)
|
|
268
|
+
graph = { [soul]: node! }
|
|
269
|
+
}
|
|
270
|
+
// Only add _["s"] if we found any signatures
|
|
271
|
+
if (graph && graph[soul] && Object.keys(signatures).length > 0) {
|
|
272
|
+
graph[soul]._["s"] = signatures
|
|
273
|
+
}
|
|
274
|
+
cb(err, graph)
|
|
275
|
+
})
|
|
276
|
+
},
|
|
277
|
+
put: (graph, cb) => {
|
|
278
|
+
if (!graph) {
|
|
279
|
+
cb("graph required")
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let count = 0
|
|
284
|
+
let finished = false
|
|
285
|
+
const ack = (err?: string | null): void => {
|
|
286
|
+
count--
|
|
287
|
+
if (finished) return
|
|
288
|
+
|
|
289
|
+
if (err) {
|
|
290
|
+
finished = true
|
|
291
|
+
cb(err)
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (count === 0) {
|
|
296
|
+
finished = true
|
|
297
|
+
cb(null)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
Object.keys(graph).forEach(soul => {
|
|
302
|
+
const node = graph[soul]!
|
|
303
|
+
Object.keys(node).forEach(key => {
|
|
304
|
+
if (key === "_") return
|
|
305
|
+
|
|
306
|
+
count++
|
|
307
|
+
const value = node[key]!
|
|
308
|
+
const state = node._[">"][key]!
|
|
309
|
+
const sig = node._["s"] && node._["s"][state]
|
|
310
|
+
const data: EncodedValue = sig
|
|
311
|
+
? [value, state, sig]
|
|
312
|
+
: [value, state]
|
|
313
|
+
radisk(soul + enq + key, data, ack)
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
},
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export default Store
|
|
321
|
+
|
package/ts/src/user.ts
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User - User authentication and management
|
|
3
|
+
* Provides user creation, authentication, and password management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as utils from "./utils.ts"
|
|
7
|
+
import Wire from "./wire.ts"
|
|
8
|
+
import SEA from "./sea.ts"
|
|
9
|
+
import type { UserIdentity, HolsterOptions, LexFilter, WireOptions } from "./schemas.ts"
|
|
10
|
+
import type { HolsterAPI } from "./holster.ts"
|
|
11
|
+
|
|
12
|
+
type WireInterface = ReturnType<typeof Wire>
|
|
13
|
+
|
|
14
|
+
export interface UserInterface {
|
|
15
|
+
is?: UserIdentity | null
|
|
16
|
+
create: (username: string, password: string, cb?: (err?: string | null) => void) => void
|
|
17
|
+
auth: (username: string, password: string, cb?: (err?: string | null) => void) => void
|
|
18
|
+
change: (
|
|
19
|
+
username: string,
|
|
20
|
+
password: string,
|
|
21
|
+
newPassword: string,
|
|
22
|
+
cb?: (err?: string | null) => void
|
|
23
|
+
) => void
|
|
24
|
+
store: (localStorage?: boolean) => void
|
|
25
|
+
recall: () => void
|
|
26
|
+
leave: () => void
|
|
27
|
+
delete: (username: string, password: string, cb?: (err?: string | null) => void) => void
|
|
28
|
+
get?(
|
|
29
|
+
keys: string | string[],
|
|
30
|
+
lex?: LexFilter | ((data: unknown) => void),
|
|
31
|
+
cb?: ((data: unknown) => void) | WireOptions,
|
|
32
|
+
_opt?: WireOptions
|
|
33
|
+
): HolsterAPI
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create User management interface
|
|
38
|
+
*/
|
|
39
|
+
const User = (opt: HolsterOptions | undefined, wire?: WireInterface): UserInterface => {
|
|
40
|
+
if (!wire) wire = Wire(opt as HolsterOptions)
|
|
41
|
+
const wait = opt?.wait ?? 5000
|
|
42
|
+
let pubs: string[] = []
|
|
43
|
+
let creating = false
|
|
44
|
+
let authing = false
|
|
45
|
+
let retries = 0
|
|
46
|
+
|
|
47
|
+
const auth = (
|
|
48
|
+
username: string,
|
|
49
|
+
password: string,
|
|
50
|
+
newPassword: string,
|
|
51
|
+
ack: (err?: string | null) => void
|
|
52
|
+
): void => {
|
|
53
|
+
const retry = (username: string): void => {
|
|
54
|
+
retries++
|
|
55
|
+
auth(username, password, newPassword, ack)
|
|
56
|
+
}
|
|
57
|
+
const done = (err?: string | null): void => {
|
|
58
|
+
pubs = []
|
|
59
|
+
retries = 0
|
|
60
|
+
authing = false
|
|
61
|
+
ack(err)
|
|
62
|
+
}
|
|
63
|
+
const next = (): void => {
|
|
64
|
+
if (pubs.length === 0) {
|
|
65
|
+
done("Wrong username or password")
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pub = pubs.shift()!
|
|
70
|
+
wire.get(
|
|
71
|
+
{ "#": pub, ".": ["auth", "pub", "epub"] },
|
|
72
|
+
async msg => {
|
|
73
|
+
if (msg.err) {
|
|
74
|
+
done(`error getting ${pub}: ${msg.err}`)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const data = msg.put && msg.put[pub]
|
|
79
|
+
if (!data || !data.auth) return next()
|
|
80
|
+
|
|
81
|
+
const authData = JSON.parse(data.auth as string) as {
|
|
82
|
+
enc: { ct: string; iv: string; s: string }
|
|
83
|
+
salt: string
|
|
84
|
+
}
|
|
85
|
+
const work = await SEA.work(password, authData.salt)
|
|
86
|
+
const dec = (await SEA.decrypt(authData.enc, work)) as
|
|
87
|
+
| { priv: string; epriv: string }
|
|
88
|
+
| null
|
|
89
|
+
if (!dec) return next()
|
|
90
|
+
|
|
91
|
+
user.is = {
|
|
92
|
+
username: username,
|
|
93
|
+
pub: data.pub as string,
|
|
94
|
+
epub: data.epub as string,
|
|
95
|
+
priv: dec.priv,
|
|
96
|
+
epriv: dec.epriv,
|
|
97
|
+
}
|
|
98
|
+
if (newPassword !== "") {
|
|
99
|
+
const salt = utils.text.random(64)
|
|
100
|
+
const newWork = await SEA.work(newPassword, salt)
|
|
101
|
+
const enc = await SEA.encrypt(dec, newWork)
|
|
102
|
+
const update = {
|
|
103
|
+
auth: JSON.stringify({ enc: enc, salt: salt }),
|
|
104
|
+
}
|
|
105
|
+
const timestamp = Date.now()
|
|
106
|
+
const sig = await SEA.signTimestamp(timestamp, user.is)
|
|
107
|
+
const graph = utils.graph(pub, update, sig!, data.pub as string, timestamp)
|
|
108
|
+
wire.put(graph, err => {
|
|
109
|
+
if (err) {
|
|
110
|
+
done(`error putting ${JSON.stringify(update)} on ${pub}: ${err}`)
|
|
111
|
+
} else {
|
|
112
|
+
done(null)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
done(null)
|
|
119
|
+
},
|
|
120
|
+
{ wait: wait }
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (retries > 9) {
|
|
125
|
+
done("Wrong username or password")
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const soul = "~@" + username
|
|
130
|
+
wire.get(
|
|
131
|
+
{ "#": soul },
|
|
132
|
+
async msg => {
|
|
133
|
+
if (msg.err) {
|
|
134
|
+
done(`error getting ${soul}: ${msg.err}`)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const data = msg.put && msg.put[soul]
|
|
139
|
+
if (!data) return retry(username)
|
|
140
|
+
|
|
141
|
+
delete (data as { _?: unknown })._
|
|
142
|
+
pubs = Object.keys(data)
|
|
143
|
+
next()
|
|
144
|
+
},
|
|
145
|
+
{ wait: wait }
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const user: UserInterface = {
|
|
150
|
+
is: null,
|
|
151
|
+
create: (username, password, cb) => {
|
|
152
|
+
const ack = (err?: string | null): void => {
|
|
153
|
+
if (cb) cb(err)
|
|
154
|
+
else if (err) console.log(err)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (creating) {
|
|
158
|
+
ack("User is already being created")
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (username === "") {
|
|
163
|
+
ack("Please provide a username")
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (password === "") {
|
|
168
|
+
ack("Please provide a password")
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
creating = true
|
|
173
|
+
|
|
174
|
+
const soul = "~@" + username
|
|
175
|
+
wire.get(
|
|
176
|
+
{ "#": soul },
|
|
177
|
+
async msg => {
|
|
178
|
+
if (msg.err) {
|
|
179
|
+
creating = false
|
|
180
|
+
ack(`error getting ${soul}: ${msg.err}`)
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (msg.put && msg.put[soul]) {
|
|
185
|
+
creating = false
|
|
186
|
+
ack("Username already exists")
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const salt = utils.text.random(64)
|
|
191
|
+
const work = await SEA.work(password, salt)
|
|
192
|
+
const pair = await SEA.pair()
|
|
193
|
+
const priv = { priv: pair.priv, epriv: pair.epriv }
|
|
194
|
+
const enc = await SEA.encrypt(priv, work)
|
|
195
|
+
const data = {
|
|
196
|
+
username: username,
|
|
197
|
+
pub: pair.pub,
|
|
198
|
+
epub: pair.epub,
|
|
199
|
+
auth: JSON.stringify({ enc: enc, salt: salt }),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const pub = "~" + pair.pub
|
|
203
|
+
const timestamp = Date.now()
|
|
204
|
+
const sig = await SEA.signTimestamp(timestamp, pair)
|
|
205
|
+
const graph = utils.graph(pub, data, sig!, pair.pub, timestamp)
|
|
206
|
+
wire.put(graph, err => {
|
|
207
|
+
creating = false
|
|
208
|
+
if (err) {
|
|
209
|
+
ack(`error putting ${JSON.stringify(data)} on ${pub}: ${err}`)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const rel = { [pub]: { "#": pub } }
|
|
214
|
+
wire.put(utils.graph(soul, rel as never), err => {
|
|
215
|
+
if (err) {
|
|
216
|
+
ack(`error putting ${JSON.stringify(rel)} on ${soul}: ${err}`)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (cb) cb(null)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
},
|
|
224
|
+
{ wait: wait }
|
|
225
|
+
)
|
|
226
|
+
},
|
|
227
|
+
auth: (username, password, cb) => {
|
|
228
|
+
const ack = (err?: string | null): void => {
|
|
229
|
+
if (cb) cb(err)
|
|
230
|
+
else if (err) console.log(err)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (authing) {
|
|
234
|
+
ack("User is already authenticating")
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
user.is = null
|
|
239
|
+
|
|
240
|
+
if (username === "") {
|
|
241
|
+
ack("Please provide a username")
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (password === "") {
|
|
246
|
+
ack("Please provide a password")
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
authing = true
|
|
251
|
+
auth(username, password, "", ack)
|
|
252
|
+
},
|
|
253
|
+
change: (username, password, newPassword, cb) => {
|
|
254
|
+
const ack = (err?: string | null): void => {
|
|
255
|
+
if (cb) cb(err)
|
|
256
|
+
else if (err) console.log(err)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (authing) {
|
|
260
|
+
ack("User is already authenticating")
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
user.is = null
|
|
265
|
+
|
|
266
|
+
if (username === "") {
|
|
267
|
+
ack("Please provide a username")
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (password === "") {
|
|
272
|
+
ack("Please provide a password")
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (newPassword === "") {
|
|
277
|
+
ack("Please provide a new password")
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
authing = true
|
|
282
|
+
auth(username, password, newPassword, ack)
|
|
283
|
+
},
|
|
284
|
+
store: (localStorage = false) => {
|
|
285
|
+
if (!user.is) {
|
|
286
|
+
console.log("Please authenticate before calling store")
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (localStorage) {
|
|
291
|
+
if (typeof globalThis.localStorage !== "undefined") {
|
|
292
|
+
globalThis.localStorage.setItem("user.is", JSON.stringify(user.is))
|
|
293
|
+
}
|
|
294
|
+
if (typeof globalThis.sessionStorage !== "undefined") {
|
|
295
|
+
globalThis.sessionStorage.removeItem("user.is")
|
|
296
|
+
}
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (typeof globalThis.sessionStorage !== "undefined") {
|
|
301
|
+
globalThis.sessionStorage.setItem("user.is", JSON.stringify(user.is))
|
|
302
|
+
}
|
|
303
|
+
if (typeof globalThis.localStorage !== "undefined") {
|
|
304
|
+
globalThis.localStorage.removeItem("user.is")
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
recall: () => {
|
|
308
|
+
if (typeof globalThis.localStorage !== "undefined") {
|
|
309
|
+
const is = globalThis.localStorage.getItem("user.is")
|
|
310
|
+
if (is) {
|
|
311
|
+
user.is = JSON.parse(is)
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (typeof globalThis.sessionStorage !== "undefined") {
|
|
317
|
+
const is = globalThis.sessionStorage.getItem("user.is")
|
|
318
|
+
if (is) {
|
|
319
|
+
user.is = JSON.parse(is)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
leave: () => {
|
|
324
|
+
user.is = null
|
|
325
|
+
if (typeof globalThis.localStorage !== "undefined") {
|
|
326
|
+
globalThis.localStorage.removeItem("user.is")
|
|
327
|
+
}
|
|
328
|
+
if (typeof globalThis.sessionStorage !== "undefined") {
|
|
329
|
+
globalThis.sessionStorage.removeItem("user.is")
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
delete: (username, password, cb) => {
|
|
333
|
+
const ack = (err?: string | null): void => {
|
|
334
|
+
if (cb) cb(err)
|
|
335
|
+
else if (err) console.log(err)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (authing) {
|
|
339
|
+
ack("User is already authenticating")
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (username === "") {
|
|
344
|
+
ack("Please provide a username")
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (password === "") {
|
|
349
|
+
ack("Please provide a password")
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
authing = true
|
|
354
|
+
auth(username, password, "", async err => {
|
|
355
|
+
if (err) {
|
|
356
|
+
ack(err)
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const data = { username: null, pub: null, epub: null, auth: null }
|
|
361
|
+
const timestamp = Date.now()
|
|
362
|
+
const sig = await SEA.signTimestamp(timestamp, user.is!)
|
|
363
|
+
const pub = "~" + user.is!.pub
|
|
364
|
+
const graph = utils.graph(pub, data as never, sig!, user.is!.pub, timestamp)
|
|
365
|
+
wire.put(graph, err => {
|
|
366
|
+
if (err) {
|
|
367
|
+
ack(`error putting null on ${pub}: ${err}`)
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
user.is = null
|
|
372
|
+
if (cb) cb(null)
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
},
|
|
376
|
+
}
|
|
377
|
+
return user
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export default User
|
|
381
|
+
|