@mblaney/holster 0.1.0 → 0.3.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/npm-publish.yml +33 -0
- package/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/examples/user.js +5 -0
- package/package.json +2 -2
- package/src/holster.js +57 -46
- package/src/index.js +0 -1
- package/src/sea-utils.js +1 -1
- package/src/store.js +1 -1
- package/src/wire.js +47 -35
- package/test/holster.put.test.js +59 -0
- package/test/holster.user.put.test.js +19 -1
package/examples/user.js
CHANGED
|
@@ -71,6 +71,11 @@ setTimeout(() => {
|
|
|
71
71
|
}, 2000)
|
|
72
72
|
|
|
73
73
|
setTimeout(() => {
|
|
74
|
+
if (!bob) {
|
|
75
|
+
console.log("Something went wrong, bob didn't log in.")
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
// Use Bob's public key to fetch shared data under his account.
|
|
75
80
|
holster.user(bob.pub).get("shared", enc => {
|
|
76
81
|
user.auth("alice", "password", async err => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mblaney/holster",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Holster is a service that allows synchronising data between devices using Node, Deno, Bun or the browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"keywords": [],
|
|
12
12
|
"author": "Malcolm Blaney",
|
|
13
|
-
"license": "Zlib
|
|
13
|
+
"license": "Zlib, MIT, Apache-2.0",
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"express": "^5.1.0",
|
|
16
16
|
"fs": "^0.0.1-security",
|
package/src/holster.js
CHANGED
|
@@ -4,12 +4,18 @@ import User from "./user.js"
|
|
|
4
4
|
import SEA from "./sea.js"
|
|
5
5
|
|
|
6
6
|
const Holster = opt => {
|
|
7
|
+
if (typeof opt === "string") opt = {peers: [opt]}
|
|
8
|
+
else if (opt instanceof Array) opt = {peers: opt}
|
|
9
|
+
else if (!utils.obj.is(opt)) opt = {}
|
|
10
|
+
|
|
7
11
|
const wire = Wire(opt)
|
|
8
12
|
const user = User(null, wire)
|
|
9
13
|
// Map callbacks since the user's callback is not passed to wire.on.
|
|
10
14
|
const map = new Map()
|
|
11
15
|
// Allow concurrent calls to the api by storing each context.
|
|
12
16
|
const allctx = new Map()
|
|
17
|
+
// Allow user queries by public key
|
|
18
|
+
let publicKey
|
|
13
19
|
|
|
14
20
|
const ok = data => {
|
|
15
21
|
return (
|
|
@@ -45,51 +51,50 @@ const Holster = opt => {
|
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
const api = ctxid => {
|
|
48
|
-
const get = (lex, soul, ack) => {
|
|
49
|
-
wire.get(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
const get = (lex, soul, ack, opt) => {
|
|
55
|
+
wire.get(
|
|
56
|
+
utils.obj.put(lex, "#", soul),
|
|
57
|
+
async msg => {
|
|
58
|
+
if (msg.err) console.log(msg.err)
|
|
59
|
+
if (msg.put && msg.put[soul]) {
|
|
60
|
+
delete msg.put[soul]._
|
|
61
|
+
delete msg.put[soul][utils.userPublicKey]
|
|
62
|
+
// Resolve any rels on the node before returning to the user.
|
|
63
|
+
for (const key of Object.keys(msg.put[soul])) {
|
|
64
|
+
const id = utils.rel.is(msg.put[soul][key])
|
|
65
|
+
if (id) {
|
|
66
|
+
const data = await new Promise(res => {
|
|
67
|
+
const _ctxid = utils.text.random()
|
|
68
|
+
allctx.set(_ctxid, {chain: [{item: null, soul: id}]})
|
|
69
|
+
api(_ctxid).next(null, res)
|
|
70
|
+
})
|
|
71
|
+
msg.put[soul][key] = data
|
|
72
|
+
}
|
|
64
73
|
}
|
|
74
|
+
ack(msg.put[soul])
|
|
75
|
+
} else {
|
|
76
|
+
// No data callback.
|
|
77
|
+
ack(null)
|
|
65
78
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
ack(null)
|
|
70
|
-
}
|
|
71
|
-
})
|
|
79
|
+
},
|
|
80
|
+
opt,
|
|
81
|
+
)
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
const graph = async (soul, data, cb) => {
|
|
75
85
|
if (!cb) cb = console.log
|
|
76
86
|
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return null
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return utils.graph(soul, data)
|
|
87
|
+
if (user.is) {
|
|
88
|
+
const signed = await SEA.sign(data, user.is)
|
|
89
|
+
return utils.graph(soul, signed.m, signed.s, user.is.pub)
|
|
84
90
|
}
|
|
85
91
|
|
|
86
|
-
if (
|
|
87
|
-
cb(`error putting data on ${soul}: user
|
|
92
|
+
if (opt.secure) {
|
|
93
|
+
cb(`error putting data on ${soul}: user required in secure mode`)
|
|
88
94
|
return null
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
|
|
92
|
-
return utils.graph(soul, signed.m, signed.s, user.is.pub)
|
|
97
|
+
return utils.graph(soul, data)
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
const done = data => {
|
|
@@ -190,7 +195,7 @@ const Holster = opt => {
|
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
return {
|
|
193
|
-
get: (key, lex, cb) => {
|
|
198
|
+
get: (key, lex, cb, opt) => {
|
|
194
199
|
if (typeof lex === "function") {
|
|
195
200
|
cb = lex
|
|
196
201
|
lex = null
|
|
@@ -202,14 +207,14 @@ const Holster = opt => {
|
|
|
202
207
|
|
|
203
208
|
// lex requires a callback as it's not included in the chain below.
|
|
204
209
|
if (lex && !cb) {
|
|
205
|
-
console.log("lex requires a callback function")
|
|
210
|
+
console.log("error lex requires a callback function")
|
|
206
211
|
return
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
// Top level keys are added to a root node so their values don't need
|
|
210
215
|
// to be objects, the user's public key is used as the root node when
|
|
211
|
-
// user.ctx is set.
|
|
212
|
-
const root = user.ctx ? user.ctx : "root"
|
|
216
|
+
// publicKey or user.ctx is set.
|
|
217
|
+
const root = publicKey ? publicKey : user.ctx ? user.ctx : "root"
|
|
213
218
|
|
|
214
219
|
ctxid = utils.text.random()
|
|
215
220
|
allctx.set(ctxid, {chain: [{item: key, soul: root}], cb: cb})
|
|
@@ -217,9 +222,9 @@ const Holster = opt => {
|
|
|
217
222
|
|
|
218
223
|
// When there's a callback need to resolve the context first.
|
|
219
224
|
const {soul} = resolve({get: lex}, done)
|
|
220
|
-
if (soul) get(lex, soul, done)
|
|
225
|
+
if (soul) get(lex, soul, done, opt)
|
|
221
226
|
},
|
|
222
|
-
next: (key, lex, cb) => {
|
|
227
|
+
next: (key, lex, cb, opt) => {
|
|
223
228
|
const ack = data => {
|
|
224
229
|
cb ? cb(data) : done(data)
|
|
225
230
|
}
|
|
@@ -229,7 +234,7 @@ const Holster = opt => {
|
|
|
229
234
|
lex = null
|
|
230
235
|
}
|
|
231
236
|
if (!ctxid) {
|
|
232
|
-
console.log("please provide a key using get(key)")
|
|
237
|
+
console.log("error please provide a key using get(key)")
|
|
233
238
|
ack(null)
|
|
234
239
|
return
|
|
235
240
|
}
|
|
@@ -241,7 +246,7 @@ const Holster = opt => {
|
|
|
241
246
|
|
|
242
247
|
// lex requires a callback as it's not included in the chain below.
|
|
243
248
|
if (lex && !cb) {
|
|
244
|
-
console.log("lex requires a callback function")
|
|
249
|
+
console.log("error lex requires a callback function")
|
|
245
250
|
return
|
|
246
251
|
}
|
|
247
252
|
|
|
@@ -262,9 +267,13 @@ const Holster = opt => {
|
|
|
262
267
|
|
|
263
268
|
// When there's a callback need to resolve the context first.
|
|
264
269
|
const {soul} = resolve({get: lex}, ack)
|
|
265
|
-
if (soul) get(lex, soul, ack)
|
|
270
|
+
if (soul) get(lex, soul, ack, opt)
|
|
266
271
|
},
|
|
267
|
-
put: (data, cb) => {
|
|
272
|
+
put: (data, set, cb) => {
|
|
273
|
+
if (typeof set === "function") {
|
|
274
|
+
cb = set
|
|
275
|
+
set = false
|
|
276
|
+
}
|
|
268
277
|
const ack = err => {
|
|
269
278
|
cb ? cb(err) : done(err)
|
|
270
279
|
}
|
|
@@ -285,6 +294,7 @@ const Holster = opt => {
|
|
|
285
294
|
ctx.cb = cb
|
|
286
295
|
cb = null
|
|
287
296
|
}
|
|
297
|
+
if (set) data = {[utils.text.random()]: data}
|
|
288
298
|
|
|
289
299
|
const result = check(data)
|
|
290
300
|
if (typeof result === "string") {
|
|
@@ -423,7 +433,7 @@ const Holster = opt => {
|
|
|
423
433
|
if (!cb) return
|
|
424
434
|
|
|
425
435
|
if (!ctxid) {
|
|
426
|
-
console.log("please provide a key using get(key)")
|
|
436
|
+
console.log("error please provide a key using get(key)")
|
|
427
437
|
cb(null)
|
|
428
438
|
return
|
|
429
439
|
}
|
|
@@ -458,7 +468,7 @@ const Holster = opt => {
|
|
|
458
468
|
},
|
|
459
469
|
off: cb => {
|
|
460
470
|
if (!ctxid) {
|
|
461
|
-
console.log("please provide a key using get(key)")
|
|
471
|
+
console.log("error please provide a key using get(key)")
|
|
462
472
|
if (cb) cb(null)
|
|
463
473
|
return
|
|
464
474
|
}
|
|
@@ -487,7 +497,8 @@ const Holster = opt => {
|
|
|
487
497
|
// will set the user context to that key, otherwise user.ctx is set when
|
|
488
498
|
// a user logs in so that their public key will be used as the root node
|
|
489
499
|
// in get(), also put() will sign all graph updates for verification.
|
|
490
|
-
if (pub)
|
|
500
|
+
if (pub) publicKey = "~" + pub
|
|
501
|
+
else publicKey = null
|
|
491
502
|
return Object.assign(user, api())
|
|
492
503
|
},
|
|
493
504
|
// Allow the wire spec to be used via holster.
|
package/src/index.js
CHANGED
package/src/sea-utils.js
CHANGED
|
@@ -2,7 +2,7 @@ import SafeBuffer from "./buffer.js"
|
|
|
2
2
|
|
|
3
3
|
const isNode = typeof document === "undefined"
|
|
4
4
|
const crypto = isNode
|
|
5
|
-
? (await import("node:crypto")).webcrypto
|
|
5
|
+
? (await import(/*webpackIgnore: true*/ "node:crypto")).webcrypto
|
|
6
6
|
: globalThis.crypto
|
|
7
7
|
export const subtle = crypto.subtle
|
|
8
8
|
|
package/src/store.js
CHANGED
|
@@ -3,7 +3,7 @@ import Radix from "./radix.js"
|
|
|
3
3
|
import * as utils from "./utils.js"
|
|
4
4
|
|
|
5
5
|
const isNode = typeof document === "undefined"
|
|
6
|
-
const fs = isNode ? await import("node:fs") : undefined
|
|
6
|
+
const fs = isNode ? await import(/*webpackIgnore: true*/ "node:fs") : undefined
|
|
7
7
|
|
|
8
8
|
// ASCII character for enquiry.
|
|
9
9
|
const enq = String.fromCharCode(5)
|
package/src/wire.js
CHANGED
|
@@ -15,8 +15,6 @@ if (typeof globalThis.WebSocket === "undefined") {
|
|
|
15
15
|
// Wire starts a websocket client or server and returns get and put methods
|
|
16
16
|
// for access to the wire spec and storage.
|
|
17
17
|
const Wire = opt => {
|
|
18
|
-
if (!utils.obj.is(opt)) opt = {}
|
|
19
|
-
|
|
20
18
|
const dup = Dup(opt.maxAge)
|
|
21
19
|
const store = Store(opt)
|
|
22
20
|
const graph = {}
|
|
@@ -207,7 +205,10 @@ const Wire = opt => {
|
|
|
207
205
|
// mock-sockets provides clients as a function that returns an array.
|
|
208
206
|
let clients = () => wss.clients()
|
|
209
207
|
if (!wss) {
|
|
210
|
-
|
|
208
|
+
const config = opt.server
|
|
209
|
+
? {server: opt.server}
|
|
210
|
+
: {port: opt.port || 8765}
|
|
211
|
+
wss = new wsModule.WebSocketServer(config)
|
|
211
212
|
clients = () => wss.clients
|
|
212
213
|
}
|
|
213
214
|
|
|
@@ -244,46 +245,57 @@ const Wire = opt => {
|
|
|
244
245
|
return api(send)
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
|
|
248
|
+
const peers = []
|
|
248
249
|
const send = data => {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
250
|
+
peers.forEach(peer => {
|
|
251
|
+
if (peer && peer.readyState === WebSocket.OPEN) {
|
|
252
|
+
peer.send(data)
|
|
253
|
+
} else {
|
|
254
|
+
console.log("websocket not available")
|
|
255
|
+
}
|
|
256
|
+
})
|
|
255
257
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
258
|
+
if (!(opt.peers instanceof Array)) {
|
|
259
|
+
opt.peers = [`ws://localhost:${opt.port || 8765}`]
|
|
260
|
+
}
|
|
261
|
+
opt.peers.forEach(peer => {
|
|
262
|
+
let ws = new WebSocket(peer)
|
|
263
|
+
peers.push(ws)
|
|
264
|
+
const start = () => {
|
|
265
|
+
if (!ws) ws = new WebSocket(peer)
|
|
266
|
+
ws.onclose = c => {
|
|
267
|
+
if (peers.indexOf(ws) !== -1) {
|
|
268
|
+
peers.splice(peers.indexOf(ws), 1)
|
|
269
|
+
}
|
|
270
|
+
ws = null
|
|
271
|
+
setTimeout(start, Math.floor(Math.random() * 5000))
|
|
272
|
+
}
|
|
273
|
+
ws.onerror = e => {
|
|
274
|
+
console.error(e)
|
|
275
|
+
}
|
|
276
|
+
ws.onmessage = m => {
|
|
277
|
+
const msg = JSON.parse(m.data)
|
|
278
|
+
if (dup.check(msg["#"])) return
|
|
268
279
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
280
|
+
dup.track(msg["#"])
|
|
281
|
+
if (msg.get) get(msg, send)
|
|
282
|
+
if (msg.put) put(msg, send)
|
|
283
|
+
send(m.data)
|
|
273
284
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
285
|
+
const id = msg["@"]
|
|
286
|
+
const cb = queue[id]
|
|
287
|
+
if (cb) {
|
|
288
|
+
delete msg["#"]
|
|
289
|
+
delete msg["@"]
|
|
290
|
+
cb(msg)
|
|
280
291
|
|
|
281
|
-
|
|
292
|
+
delete queue[id]
|
|
293
|
+
}
|
|
282
294
|
}
|
|
283
295
|
}
|
|
284
|
-
|
|
296
|
+
start()
|
|
297
|
+
})
|
|
285
298
|
|
|
286
|
-
start()
|
|
287
299
|
return api(send)
|
|
288
300
|
}
|
|
289
301
|
|
package/test/holster.put.test.js
CHANGED
|
@@ -269,6 +269,65 @@ describe("holster.put", () => {
|
|
|
269
269
|
})
|
|
270
270
|
})
|
|
271
271
|
|
|
272
|
+
test("holster put string with set", (t, done) => {
|
|
273
|
+
holster.get("set").put("first", true, err => {
|
|
274
|
+
assert.equal(err, null)
|
|
275
|
+
|
|
276
|
+
holster.get("set", data => {
|
|
277
|
+
assert.equal(Object.values(data)[0], "first")
|
|
278
|
+
|
|
279
|
+
setTimeout(() => {
|
|
280
|
+
holster.get("set").put("second", true, err => {
|
|
281
|
+
assert.equal(err, null)
|
|
282
|
+
|
|
283
|
+
holster.get("set", data => {
|
|
284
|
+
for (const value of Object.values(data)) {
|
|
285
|
+
assert.ok(value === "first" || value === "second")
|
|
286
|
+
}
|
|
287
|
+
done()
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
}, 2)
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test("holster put object with set", (t, done) => {
|
|
296
|
+
const set1 = {
|
|
297
|
+
key: "value 1",
|
|
298
|
+
child: "child value 1",
|
|
299
|
+
}
|
|
300
|
+
const set2 = {
|
|
301
|
+
key: "value 2",
|
|
302
|
+
child: "child value 2",
|
|
303
|
+
}
|
|
304
|
+
holster.get("set2").put(set1, true, err => {
|
|
305
|
+
assert.equal(err, null)
|
|
306
|
+
|
|
307
|
+
holster.get("set2", data => {
|
|
308
|
+
assert.deepEqual(Object.values(data)[0], set1)
|
|
309
|
+
|
|
310
|
+
setTimeout(() => {
|
|
311
|
+
holster.get("set2").put(set2, true, err => {
|
|
312
|
+
assert.equal(err, null)
|
|
313
|
+
|
|
314
|
+
holster.get("set2", data => {
|
|
315
|
+
for (const value of Object.values(data)) {
|
|
316
|
+
if (value.key === "value 1") {
|
|
317
|
+
assert.deepEqual(value, set1)
|
|
318
|
+
}
|
|
319
|
+
if (value.key === "value 2") {
|
|
320
|
+
assert.deepEqual(value, set2)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
done()
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
}, 2)
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
272
331
|
test("cleanup", (t, done) => {
|
|
273
332
|
fs.rm("test/holster.put", {recursive: true, force: true}, err => {
|
|
274
333
|
assert.equal(err, null)
|
|
@@ -291,7 +291,25 @@ describe("holster.user.put", () => {
|
|
|
291
291
|
})
|
|
292
292
|
})
|
|
293
293
|
|
|
294
|
-
test("get number using public key", (t, done) => {
|
|
294
|
+
test("get number using public key - logged in", (t, done) => {
|
|
295
|
+
let ok = false
|
|
296
|
+
|
|
297
|
+
holster.user(alice).get("pi", data => {
|
|
298
|
+
assert.equal(data, 3.14159)
|
|
299
|
+
if (ok) done()
|
|
300
|
+
else ok = true
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// Not reusing the existing user here resets the stored public key used
|
|
304
|
+
// above, but that is ok because they use separate contexts.
|
|
305
|
+
holster.user().get("pi", data => {
|
|
306
|
+
assert.equal(data, 3.14159)
|
|
307
|
+
if (ok) done()
|
|
308
|
+
else ok = true
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test("get number using public key - logged out", (t, done) => {
|
|
295
313
|
user.leave()
|
|
296
314
|
|
|
297
315
|
holster.user(alice).get("pi", data => {
|