feat(modules): deepen trust, experimental, routing, and search
Hyper-P2P Module Tests / unit-all (push) Failing after 1h17m5s
Hyper-P2P Module Tests / unit-all (push) Failing after 1h17m5s
Session revoke and encrypted-topic JSON helpers; silence/void channel snapshots; relay tunnel route listing; grow-only set algebra; CPU top donors; attestation chain slice/export; fulltext bulk and top terms. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -66,6 +66,25 @@ class HyperP2PSilenceProtocol extends EventEmitter {
|
|||||||
return { absent: [...this._absent.values()] }
|
return { absent: [...this._absent.values()] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listAbsent () {
|
||||||
|
return [...this._absent.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
absentCount () {
|
||||||
|
return this._absent.size
|
||||||
|
}
|
||||||
|
|
||||||
|
clear () {
|
||||||
|
const n = this._seen.size + this._absent.size
|
||||||
|
this._seen.clear()
|
||||||
|
this._absent.clear()
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
revokeAbsent (id) {
|
||||||
|
return this._absent.delete(String(id))
|
||||||
|
}
|
||||||
|
|
||||||
async ready () {
|
async ready () {
|
||||||
if (this.swarm || !this.topic) return this
|
if (this.swarm || !this.topic) return this
|
||||||
await initModuleSwarm(this, {
|
await initModuleSwarm(this, {
|
||||||
|
|||||||
@@ -29,6 +29,19 @@ test('silence-protocol: merge', async (t) => {
|
|||||||
await a.close()
|
await a.close()
|
||||||
await b.close()
|
await b.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('silence-protocol: listAbsent clear revoke', async (t) => {
|
||||||
|
const s = new HyperP2PSilenceProtocol()
|
||||||
|
s.assertAbsent('a')
|
||||||
|
s.assertAbsent('b')
|
||||||
|
t.is(s.absentCount(), 2)
|
||||||
|
t.is(s.listAbsent().length, 2)
|
||||||
|
t.ok(s.revokeAbsent('a'))
|
||||||
|
t.is(s.absentCount(), 1)
|
||||||
|
s.clear()
|
||||||
|
t.is(s.absentCount(), 0)
|
||||||
|
await s.close()
|
||||||
|
})
|
||||||
test('hyper-p2p-silence-protocol: close without leak', async (t) => {
|
test('hyper-p2p-silence-protocol: close without leak', async (t) => {
|
||||||
const m = new HyperP2PSilenceProtocol()
|
const m = new HyperP2PSilenceProtocol()
|
||||||
await m.close()
|
await m.close()
|
||||||
|
|||||||
@@ -40,6 +40,32 @@ class HyperP2PVoidChannel extends EventEmitter {
|
|||||||
return [...new Set([...this._subs.keys(), ...this._voids.keys()])]
|
return [...new Set([...this._subs.keys(), ...this._voids.keys()])]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearVoid (channel) {
|
||||||
|
return this._voids.delete(String(channel))
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberCount () {
|
||||||
|
return this._subs.size
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeSnapshot (snapshot) {
|
||||||
|
if (!snapshot || !Array.isArray(snapshot.voids)) return 0
|
||||||
|
let n = 0
|
||||||
|
for (const msg of snapshot.voids) {
|
||||||
|
if (!msg || !msg.channel) continue
|
||||||
|
const cur = this._voids.get(msg.channel)
|
||||||
|
if (!cur || msg.at > cur.at) {
|
||||||
|
this._voids.set(msg.channel, { ...msg, voided: true })
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
toSnapshot () {
|
||||||
|
return { voids: [...this._voids.values()] }
|
||||||
|
}
|
||||||
|
|
||||||
subscribeVoid (channel, fn) {
|
subscribeVoid (channel, fn) {
|
||||||
assertNonEmpty(channel, 'channel')
|
assertNonEmpty(channel, 'channel')
|
||||||
if (typeof fn !== 'function') throw new Error('fn must be a function')
|
if (typeof fn !== 'function') throw new Error('fn must be a function')
|
||||||
|
|||||||
@@ -114,6 +114,29 @@ class HyperP2PFulltextLite extends EventEmitter {
|
|||||||
return [...this._docs.values()]
|
return [...this._docs.values()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bulkAdd (entries) {
|
||||||
|
if (!Array.isArray(entries)) throw new Error('entries array required')
|
||||||
|
let n = 0
|
||||||
|
for (const e of entries) {
|
||||||
|
if (e && e.id != null) {
|
||||||
|
this.addDocument(e.id, e.text || '')
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
termCount () {
|
||||||
|
return this._index.size
|
||||||
|
}
|
||||||
|
|
||||||
|
topTerms (limit = 10) {
|
||||||
|
return [...this._index.entries()]
|
||||||
|
.map(([term, set]) => ({ term, docs: set.size }))
|
||||||
|
.sort((a, b) => b.docs - a.docs)
|
||||||
|
.slice(0, limit)
|
||||||
|
}
|
||||||
|
|
||||||
getStats () {
|
getStats () {
|
||||||
return {
|
return {
|
||||||
...this._stats,
|
...this._stats,
|
||||||
|
|||||||
@@ -72,6 +72,27 @@ class HyperP2PRelayTunnel extends EventEmitter {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unpublish (rendezvousKey = null) {
|
||||||
|
const key = rendezvousKey || this.rendezvous
|
||||||
|
const had = this._routes.delete(key)
|
||||||
|
if (had && this._peerMsgs) gossipSend(this, { type: 'unpublish', rendezvous: key })
|
||||||
|
return had
|
||||||
|
}
|
||||||
|
|
||||||
|
listRoutes () {
|
||||||
|
return [...this._routes.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
getTunnel (tunnelId) {
|
||||||
|
return this._pending.get(tunnelId) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
listTunnels (state = null) {
|
||||||
|
const all = [...this._pending.values()]
|
||||||
|
if (!state) return all
|
||||||
|
return all.filter((t) => t.state === state)
|
||||||
|
}
|
||||||
|
|
||||||
async ready () {
|
async ready () {
|
||||||
if (this.swarm || !this.topic) return this
|
if (this.swarm || !this.topic) return this
|
||||||
await initModuleSwarm(this, {
|
await initModuleSwarm(this, {
|
||||||
|
|||||||
@@ -55,6 +55,24 @@ class HyperP2PCrdtGrowOnlySet extends EventEmitter {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
union (other) {
|
||||||
|
if (!other || typeof other.values !== 'function') throw new Error('other set required')
|
||||||
|
return this.merge(other.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubsetOf (other) {
|
||||||
|
if (!other || typeof other.has !== 'function') throw new Error('other set required')
|
||||||
|
for (const el of this._added) {
|
||||||
|
if (!other.has(el)) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
difference (other) {
|
||||||
|
if (!other || typeof other.has !== 'function') throw new Error('other set required')
|
||||||
|
return this.values().filter((el) => !other.has(el))
|
||||||
|
}
|
||||||
|
|
||||||
_gossip (data) {
|
_gossip (data) {
|
||||||
if (!this._peerMsgs) return
|
if (!this._peerMsgs) return
|
||||||
gossipSend(this, data)
|
gossipSend(this, data)
|
||||||
|
|||||||
@@ -62,6 +62,20 @@ class HyperP2PCpuShare extends EventEmitter {
|
|||||||
return this.balance(peerId) >= cpuMs
|
return this.balance(peerId) >= cpuMs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
topDonors (n = 5) {
|
||||||
|
return [...this._balances.entries()]
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, n)
|
||||||
|
.map(([peerId, balance]) => ({ peerId, balance }))
|
||||||
|
}
|
||||||
|
|
||||||
|
resetBalance (peerId) {
|
||||||
|
const id = normalizePeerId(peerId)
|
||||||
|
const had = this._balances.delete(id)
|
||||||
|
if (had) this.emit('reset', { peerId: id })
|
||||||
|
return had
|
||||||
|
}
|
||||||
|
|
||||||
async ready () {
|
async ready () {
|
||||||
if (this.swarm || !this.topic) return this
|
if (this.swarm || !this.topic) return this
|
||||||
await initModuleSwarm(this, {
|
await initModuleSwarm(this, {
|
||||||
|
|||||||
@@ -59,6 +59,29 @@ class HyperP2PAttestationChain extends EventEmitter {
|
|||||||
|
|
||||||
chainLength () { return this._chain.length }
|
chainLength () { return this._chain.length }
|
||||||
|
|
||||||
|
at (index) {
|
||||||
|
if (index < 0 || index >= this._chain.length) return null
|
||||||
|
return { ...this._chain[index] }
|
||||||
|
}
|
||||||
|
|
||||||
|
slice (from = 0, to = this._chain.length) {
|
||||||
|
return this._chain.slice(from, to).map((l) => ({ ...l }))
|
||||||
|
}
|
||||||
|
|
||||||
|
payloads () {
|
||||||
|
return this._chain.map((l) => l.payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON () {
|
||||||
|
return { chain: this.slice() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fromJSON (snap) {
|
||||||
|
if (!snap || !Array.isArray(snap.chain)) throw new Error('invalid snapshot')
|
||||||
|
this._chain = snap.chain.map((l) => ({ ...l }))
|
||||||
|
return this._chain.length
|
||||||
|
}
|
||||||
|
|
||||||
async ready () {
|
async ready () {
|
||||||
if (this.swarm || !this.topic) return this
|
if (this.swarm || !this.topic) return this
|
||||||
await initModuleSwarm(this, {
|
await initModuleSwarm(this, {
|
||||||
|
|||||||
@@ -61,6 +61,38 @@ class HyperP2PEncryptedTopic extends EventEmitter {
|
|||||||
return plain
|
return plain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasTopic (topicId) {
|
||||||
|
return this._topics.has(String(topicId))
|
||||||
|
}
|
||||||
|
|
||||||
|
listTopics () {
|
||||||
|
return [...this._topics.keys()]
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterTopic (topicId) {
|
||||||
|
assertNonEmpty(topicId, 'topicId')
|
||||||
|
const had = this._topics.delete(topicId)
|
||||||
|
if (had) this.emit('unregistered', { topicId })
|
||||||
|
return had
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateHint (topicId, newHint) {
|
||||||
|
assertNonEmpty(topicId, 'topicId')
|
||||||
|
assertNonEmpty(newHint, 'newHint')
|
||||||
|
if (!this._topics.has(topicId)) throw new Error(`topic not registered: ${topicId}`)
|
||||||
|
return this.registerTopic(topicId, newHint)
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptJSON (topicId, obj) {
|
||||||
|
const json = b4a.from(JSON.stringify(obj))
|
||||||
|
return this.encryptPayload(topicId, json)
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptJSON (topicId, buf) {
|
||||||
|
const plain = this.decryptPayload(topicId, buf)
|
||||||
|
return JSON.parse(b4a.toString(plain))
|
||||||
|
}
|
||||||
|
|
||||||
_gossip (data) {
|
_gossip (data) {
|
||||||
if (!this._peerMsgs) return
|
if (!this._peerMsgs) return
|
||||||
gossipSend(this, data)
|
gossipSend(this, data)
|
||||||
|
|||||||
@@ -37,3 +37,15 @@ test('getStats', async (t) => {
|
|||||||
t.is(m.getStats().encrypted, 1)
|
t.is(m.getStats().encrypted, 1)
|
||||||
await m.close()
|
await m.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('json list unregister', async (t) => {
|
||||||
|
const m = new HyperP2PEncryptedTopic()
|
||||||
|
m.registerTopic('t', 'hint')
|
||||||
|
const buf = m.encryptJSON('t', { ok: 1 })
|
||||||
|
t.is(m.decryptJSON('t', buf).ok, 1)
|
||||||
|
t.ok(m.hasTopic('t'))
|
||||||
|
t.alike(m.listTopics(), ['t'])
|
||||||
|
t.ok(m.unregisterTopic('t'))
|
||||||
|
t.not(m.hasTopic('t'))
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|||||||
@@ -42,6 +42,26 @@ class HyperP2PSessionRotation extends EventEmitter {
|
|||||||
return [...this._sessions.values()]
|
return [...this._sessions.values()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
revokeSession (sessionId) {
|
||||||
|
assertNonEmpty(sessionId, 'sessionId')
|
||||||
|
const had = this._sessions.delete(sessionId)
|
||||||
|
if (had) {
|
||||||
|
this._gossip({ type: 'session-revoke', sessionId, at: Date.now() })
|
||||||
|
this.emit('revoked', { sessionId })
|
||||||
|
}
|
||||||
|
return had
|
||||||
|
}
|
||||||
|
|
||||||
|
validateToken (sessionId, token) {
|
||||||
|
const s = this.getSession(sessionId)
|
||||||
|
return !!(s && s.token === token)
|
||||||
|
}
|
||||||
|
|
||||||
|
latestVersion (sessionId) {
|
||||||
|
const s = this.getSession(sessionId)
|
||||||
|
return s ? s.version : 0
|
||||||
|
}
|
||||||
|
|
||||||
_gossip (data) {
|
_gossip (data) {
|
||||||
if (!this._peerMsgs) return
|
if (!this._peerMsgs) return
|
||||||
gossipSend(this, data)
|
gossipSend(this, data)
|
||||||
@@ -49,7 +69,13 @@ class HyperP2PSessionRotation extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onGossip (d) {
|
_onGossip (d) {
|
||||||
if (!d || d.type !== 'session-rotate' || !d.sessionId) return
|
if (!d || !d.sessionId) return
|
||||||
|
if (d.type === 'session-revoke') {
|
||||||
|
this._stats.gossipIn++
|
||||||
|
this._sessions.delete(d.sessionId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (d.type !== 'session-rotate') return
|
||||||
this._stats.gossipIn++
|
this._stats.gossipIn++
|
||||||
const prev = this._sessions.get(d.sessionId)
|
const prev = this._sessions.get(d.sessionId)
|
||||||
if (!prev || d.version >= prev.version) {
|
if (!prev || d.version >= prev.version) {
|
||||||
|
|||||||
@@ -36,3 +36,14 @@ test('getStats', async (t) => {
|
|||||||
t.is(m.getStats().rotations, 1)
|
t.is(m.getStats().rotations, 1)
|
||||||
await m.close()
|
await m.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('validateToken revokeSession', async (t) => {
|
||||||
|
const m = new HyperP2PSessionRotation()
|
||||||
|
m.rotateSession('s', 'secret')
|
||||||
|
t.ok(m.validateToken('s', 'secret'))
|
||||||
|
t.not(m.validateToken('s', 'wrong'))
|
||||||
|
t.is(m.latestVersion('s'), 1)
|
||||||
|
t.ok(m.revokeSession('s'))
|
||||||
|
t.is(m.getSession('s'), null)
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user