@mblaney/holster 1.0.31 → 1.0.33
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/package.json +1 -1
- package/src/ham.js +47 -9
- package/src/holster.js +1 -1
- package/src/user.js +4 -3
- package/src/utils.js +1 -14
- package/src/wire.js +3 -3
- package/test/holster.user.get.test.js +1 -0
- package/test/holster.user.lex.test.js +1 -0
- package/test/holster.user.off.test.js +1 -0
- package/test/holster.user.on.test.js +6 -1
- package/test/holster.user.put.test.js +1 -0
- package/test/system/user-chained-on.test.js +5 -1
- package/test/system/user-concurrent-listeners.test.js +1 -0
- package/test/system/user-listener-before-data.test.js +1 -0
- package/test/user.test.js +1 -1
package/package.json
CHANGED
package/src/ham.js
CHANGED
|
@@ -11,11 +11,10 @@ const Ham = (state, currentState, value, currentValue, signed = false) => {
|
|
|
11
11
|
|
|
12
12
|
if (state > currentState) return {incoming: true}
|
|
13
13
|
|
|
14
|
-
// state is equal to currentState
|
|
15
|
-
//
|
|
16
|
-
// owner can only create one value per timestamp.
|
|
14
|
+
// state is equal to currentState. Old data is expected to propagate through
|
|
15
|
+
// the network, but conflicting values with same signed timestamp are ignored
|
|
16
|
+
// since the owner can only create one value per timestamp.
|
|
17
17
|
if (signed && value !== currentValue) {
|
|
18
|
-
console.log(`Signed timestamp ${state}: reject conflicting value`)
|
|
19
18
|
return {historical: true}
|
|
20
19
|
}
|
|
21
20
|
|
|
@@ -96,15 +95,36 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
96
95
|
const signedTimestamps = new Set()
|
|
97
96
|
const signedProperties = new Set()
|
|
98
97
|
|
|
99
|
-
//
|
|
98
|
+
// Skip verification of timestamps that are older than what's in the graph
|
|
99
|
+
// to prevent old signatures from distributed messages causing conflicts
|
|
100
|
+
const incomingTimestamps = Object.entries(node._[">"] || {})
|
|
101
|
+
|
|
100
102
|
for (const [key, sig] of Object.entries(node._["s"] || {})) {
|
|
101
103
|
const asNumber = Number(key)
|
|
102
104
|
if (!isNaN(asNumber) && asNumber > 0) {
|
|
105
|
+
// Check if this timestamp is for properties that already have newer
|
|
106
|
+
// versions in graph.
|
|
107
|
+
const hasNewerInGraph = incomingTimestamps.some(([prop, ts]) => {
|
|
108
|
+
if (Number(ts) === asNumber) {
|
|
109
|
+
const graphState =
|
|
110
|
+
graph[soul] && graph[soul]._[">"] ? graph[soul]._[">"][prop] : 0
|
|
111
|
+
// Skip verification if graph has a newer timestamp for property.
|
|
112
|
+
if (graphState > asNumber) {
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return false
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
if (hasNewerInGraph) {
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
|
|
103
123
|
const isValid = await SEA.verifyTimestamp(asNumber, sig, pub)
|
|
104
124
|
if (isValid) {
|
|
105
125
|
signedTimestamps.add(asNumber)
|
|
106
126
|
// Add all properties with this timestamp to signed set
|
|
107
|
-
for (const [prop, ts] of
|
|
127
|
+
for (const [prop, ts] of incomingTimestamps) {
|
|
108
128
|
if (Number(ts) === asNumber) {
|
|
109
129
|
signedProperties.add(prop)
|
|
110
130
|
}
|
|
@@ -113,9 +133,27 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
113
133
|
}
|
|
114
134
|
}
|
|
115
135
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
136
|
+
// TODO: Remove per-property signature verification after old data has
|
|
137
|
+
// been cleared. Only check per-property signatures if there are
|
|
138
|
+
// properties without timestamp signatures.
|
|
139
|
+
const propertiesWithoutTimestampSigs = Object.keys(node).filter(k => {
|
|
140
|
+
if (k === "_" || k === utils.userPublicKey) return false
|
|
141
|
+
return !incomingTimestamps.some(
|
|
142
|
+
([prop, ts]) => prop === k && signedTimestamps.has(Number(ts)),
|
|
143
|
+
)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
if (propertiesWithoutTimestampSigs.length > 0) {
|
|
147
|
+
const nodeToVerify = {_: node._}
|
|
148
|
+
for (const prop of propertiesWithoutTimestampSigs) {
|
|
149
|
+
nodeToVerify[prop] = node[prop]
|
|
150
|
+
}
|
|
151
|
+
const perPropertySignedProps = await SEA.verifyProperties(
|
|
152
|
+
nodeToVerify,
|
|
153
|
+
pub,
|
|
154
|
+
)
|
|
155
|
+
perPropertySignedProps.forEach(prop => signedProperties.add(prop))
|
|
156
|
+
}
|
|
119
157
|
|
|
120
158
|
// If no properties verified, skip this update
|
|
121
159
|
if (signedProperties.size === 0) {
|
package/src/holster.js
CHANGED
|
@@ -9,7 +9,7 @@ const Holster = opt => {
|
|
|
9
9
|
else if (!utils.obj.is(opt)) opt = {}
|
|
10
10
|
|
|
11
11
|
const wire = Wire(opt)
|
|
12
|
-
const user = User(
|
|
12
|
+
const user = User(opt, wire)
|
|
13
13
|
// Map callbacks since the user's callback is not passed to wire.on.
|
|
14
14
|
const map = new Map()
|
|
15
15
|
// Allow concurrent calls to the api by storing each context.
|
package/src/user.js
CHANGED
|
@@ -5,6 +5,7 @@ import {userPublicKey, userSignature} from "./utils.js"
|
|
|
5
5
|
|
|
6
6
|
const User = (opt, wire) => {
|
|
7
7
|
if (!wire) wire = Wire(opt)
|
|
8
|
+
const wait = opt?.wait ?? 5000
|
|
8
9
|
let pubs = []
|
|
9
10
|
let creating = false
|
|
10
11
|
let authing = false
|
|
@@ -75,7 +76,7 @@ const User = (opt, wire) => {
|
|
|
75
76
|
|
|
76
77
|
done(null)
|
|
77
78
|
},
|
|
78
|
-
{wait:
|
|
79
|
+
{wait: wait},
|
|
79
80
|
)
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -102,7 +103,7 @@ const User = (opt, wire) => {
|
|
|
102
103
|
pubs = Object.keys(data)
|
|
103
104
|
next()
|
|
104
105
|
},
|
|
105
|
-
{wait:
|
|
106
|
+
{wait: wait},
|
|
106
107
|
)
|
|
107
108
|
}
|
|
108
109
|
|
|
@@ -182,7 +183,7 @@ const User = (opt, wire) => {
|
|
|
182
183
|
})
|
|
183
184
|
})
|
|
184
185
|
},
|
|
185
|
-
{wait:
|
|
186
|
+
{wait: wait},
|
|
186
187
|
)
|
|
187
188
|
},
|
|
188
189
|
auth: (username, password, cb) => {
|
package/src/utils.js
CHANGED
|
@@ -89,21 +89,8 @@ export const graph = (soul, data, sig, pub, timestamp) => {
|
|
|
89
89
|
g[soul]._[">"][key] = timestamp || Date.now()
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
// Store signatures in node._["s"]
|
|
93
|
-
// sig can be:
|
|
94
|
-
// - object: per-property signatures (legacy format)
|
|
95
|
-
// - string: timestamp signature (new format)
|
|
96
92
|
if (sig && pub && timestamp) {
|
|
97
|
-
g[soul]._["s"] = {}
|
|
98
|
-
if (typeof sig === "string") {
|
|
99
|
-
// Timestamp signature: store with timestamp as key
|
|
100
|
-
g[soul]._["s"][timestamp] = sig
|
|
101
|
-
} else if (typeof sig === "object") {
|
|
102
|
-
// Per-property signatures: store with property names as keys
|
|
103
|
-
for (const [key, signature] of Object.entries(sig)) {
|
|
104
|
-
g[soul]._["s"][key] = signature
|
|
105
|
-
}
|
|
106
|
-
}
|
|
93
|
+
g[soul]._["s"] = {[timestamp]: sig}
|
|
107
94
|
g[soul][userPublicKey] = pub
|
|
108
95
|
g[soul]._[">"][userPublicKey] = timestamp
|
|
109
96
|
}
|
package/src/wire.js
CHANGED
|
@@ -589,11 +589,11 @@ const Wire = opt => {
|
|
|
589
589
|
return
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
-
const processMessage = () => {
|
|
592
|
+
const processMessage = async () => {
|
|
593
593
|
dup.track(msg["#"])
|
|
594
594
|
|
|
595
595
|
if (msg.get) get(msg, send)
|
|
596
|
-
if (msg.put) put(msg, send)
|
|
596
|
+
if (msg.put) await put(msg, send)
|
|
597
597
|
send(data, isBinary)
|
|
598
598
|
|
|
599
599
|
const id = msg["@"]
|
|
@@ -807,7 +807,7 @@ const Wire = opt => {
|
|
|
807
807
|
if (Object.keys(filteredPut).length > 0) {
|
|
808
808
|
// Create filtered message for Ham.mix with original message ID
|
|
809
809
|
const filteredMsg = {put: filteredPut, "#": msg["#"]}
|
|
810
|
-
put(filteredMsg, send)
|
|
810
|
+
await put(filteredMsg, send)
|
|
811
811
|
}
|
|
812
812
|
}
|
|
813
813
|
|
|
@@ -6,7 +6,12 @@ import Holster from "../src/holster.js"
|
|
|
6
6
|
|
|
7
7
|
describe("holster.user.on", () => {
|
|
8
8
|
const wss = new Server("ws://localhost:1234")
|
|
9
|
-
const holster = Holster({
|
|
9
|
+
const holster = Holster({
|
|
10
|
+
file: "test/holster.user.on",
|
|
11
|
+
wss: wss,
|
|
12
|
+
maxAge: 100,
|
|
13
|
+
wait: 500,
|
|
14
|
+
})
|
|
10
15
|
const user = holster.user()
|
|
11
16
|
|
|
12
17
|
test("user create", (t, done) => {
|
|
@@ -6,7 +6,11 @@ import Holster from "../../src/holster.js"
|
|
|
6
6
|
|
|
7
7
|
describe("system - user chained get with on", () => {
|
|
8
8
|
const wss = new Server("ws://localhost:9012")
|
|
9
|
-
const holster = Holster({
|
|
9
|
+
const holster = Holster({
|
|
10
|
+
file: "test/system/user-chained-on",
|
|
11
|
+
wss: wss,
|
|
12
|
+
wait: 500,
|
|
13
|
+
})
|
|
10
14
|
const user = holster.user()
|
|
11
15
|
|
|
12
16
|
test("user create and auth", (t, done) => {
|
package/test/user.test.js
CHANGED
|
@@ -6,7 +6,7 @@ import User from "../src/user.js"
|
|
|
6
6
|
|
|
7
7
|
describe("user", () => {
|
|
8
8
|
const wss = new Server("ws://localhost:1234")
|
|
9
|
-
const user = User({file: "test/user", wss: wss, maxAge: 100})
|
|
9
|
+
const user = User({file: "test/user", wss: wss, maxAge: 100, wait: 500})
|
|
10
10
|
|
|
11
11
|
test("create", (t, done) => {
|
|
12
12
|
user.create("alice", "password", err => {
|