@mblaney/holster 1.0.30 → 1.0.32
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/buffer.js +8 -1
- package/src/ham.js +47 -9
- package/src/user.js +1 -40
- package/src/utils.js +1 -14
- package/src/wire.js +1 -1
package/package.json
CHANGED
package/src/buffer.js
CHANGED
|
@@ -161,6 +161,14 @@ const processArrayLikeInput = input => {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
bytes = new Uint8Array(input)
|
|
165
|
+
} else if (
|
|
166
|
+
typeof input === "object" &&
|
|
167
|
+
input !== null &&
|
|
168
|
+
typeof input.byteLength === "number"
|
|
169
|
+
) {
|
|
170
|
+
// Handle ArrayBuffer from different contexts (e.g. jsdom)
|
|
171
|
+
validateBufferSize(input.byteLength, "ArrayBuffer-like processing")
|
|
164
172
|
bytes = new Uint8Array(input)
|
|
165
173
|
} else if (
|
|
166
174
|
typeof input === "object" &&
|
|
@@ -184,7 +192,6 @@ const processArrayLikeInput = input => {
|
|
|
184
192
|
)
|
|
185
193
|
}
|
|
186
194
|
}
|
|
187
|
-
|
|
188
195
|
bytes = new Uint8Array(arr)
|
|
189
196
|
} else {
|
|
190
197
|
throw new TypeError("Unsupported input type: " + typeof input)
|
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/user.js
CHANGED
|
@@ -73,46 +73,7 @@ const User = (opt, wire) => {
|
|
|
73
73
|
return
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
// Can be enabled by setting localStorage.setItem("holster_migrate_signatures", "true")
|
|
78
|
-
const migrationEnabled =
|
|
79
|
-
typeof globalThis.localStorage !== "undefined" &&
|
|
80
|
-
globalThis.localStorage.getItem("holster_migrate_signatures") ===
|
|
81
|
-
"true"
|
|
82
|
-
|
|
83
|
-
if (migrationEnabled) {
|
|
84
|
-
const migrationData = {}
|
|
85
|
-
for (const key of Object.keys(data)) {
|
|
86
|
-
if (
|
|
87
|
-
key !== "_" &&
|
|
88
|
-
key !== userPublicKey &&
|
|
89
|
-
key !== userSignature
|
|
90
|
-
) {
|
|
91
|
-
migrationData[key] = data[key]
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// Sign the timestamp for migration
|
|
95
|
-
const timestamp = Date.now()
|
|
96
|
-
const sig = await SEA.signTimestamp(timestamp, user.is)
|
|
97
|
-
const graph = utils.graph(
|
|
98
|
-
pub,
|
|
99
|
-
migrationData,
|
|
100
|
-
sig,
|
|
101
|
-
data.pub,
|
|
102
|
-
timestamp,
|
|
103
|
-
)
|
|
104
|
-
wire.put(graph, err => {
|
|
105
|
-
if (err) {
|
|
106
|
-
// Log migration error but don't fail authentication
|
|
107
|
-
console.log(
|
|
108
|
-
`warning: failed to migrate signatures on login: ${err}`,
|
|
109
|
-
)
|
|
110
|
-
}
|
|
111
|
-
done(null)
|
|
112
|
-
})
|
|
113
|
-
} else {
|
|
114
|
-
done(null)
|
|
115
|
-
}
|
|
76
|
+
done(null)
|
|
116
77
|
},
|
|
117
78
|
{wait: 5000},
|
|
118
79
|
)
|
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
|
@@ -732,7 +732,7 @@ const Wire = opt => {
|
|
|
732
732
|
queueProcessor = setTimeout(processQueue, 50)
|
|
733
733
|
}
|
|
734
734
|
}
|
|
735
|
-
if (!(opt.peers instanceof Array)) {
|
|
735
|
+
if (!(opt.peers instanceof Array) || opt.peers.length === 0) {
|
|
736
736
|
opt.peers = [`ws://localhost:${opt.port || 8765}`]
|
|
737
737
|
}
|
|
738
738
|
opt.peers.forEach(peer => {
|