This commit is contained in:
Raven Scott
2026-05-20 22:56:32 -04:00
parent b8c335adee
commit dd384eb944
321 changed files with 7484 additions and 3793 deletions
@@ -1,28 +1,34 @@
# hyper-p2p-encrypted-topic
**Status:** scaffold (`0.0.0-scaffold`) · **Protocol:** `encrypted-topic/v1` · **Wave:** 8
Production module: Topic encryption demo.
Encrypted topic wrapper.
**Protocol:** `encrypted-topic/v1`
## Holepunch references (inspiration only)
## When to use
- `blind-encryption-sodium`
Topic-scoped payload crypto.
> This module composes on Hyperswarm/Hypercore — it does **not** re-implement upstream packages.
## When not to use
## Composes with
Production AEAD.
- `hyper-p2p-presence`
## Quick start
## Planned API
```js
const { HyperP2PEncryptedTopic } = require('hyper-p2p-encrypted-topic')
const m = new HyperP2PEncryptedTopic()
m.registerTopic('t','hint')
await m.ready()
await m.close()
```
- `constructor(opts)` — topic, optional keyPair
- `getStats()` — scaffold counters
- `ready()` — no-op until implemented
- Domain methods — throw `not implemented: scaffold` until Wave 8+ pass
## Docs
## Layout
- [docs/api.md](docs/api.md)
- [docs/architecture.md](docs/architecture.md)
`modules/trust-security/hyper-p2p-encrypted-topic/`
## Test
See [`modules/_shared/MODULE_SYSTEM.md`](../../_shared/MODULE_SYSTEM.md).
```bash
npm install && npm test
```
@@ -1,23 +1,26 @@
# hyper-p2p-encrypted-topic API
# API: hyper-p2p-encrypted-topic
**Status:** scaffold · **Protocol:** `encrypted-topic/v1`
**Protocol:** `encrypted-topic/v1` · **Export:** `HyperP2PEncryptedTopic`
## Class `HyperP2PEncryptedTopic`
## Methods
Scaffold stub — methods throw `not implemented: scaffold` until promoted to production tier.
### `registerTopic(...)`
### `constructor(opts?)`
Domain API.
### `getStats()`
### `encryptPayload(...)`
Returns `{ created, errors, protocol, tier: 'scaffold' }`.
Domain API.
### `ready()`
### `decryptPayload(...)`
Resolves immediately (no-op).
Domain API.
## Wire (planned)
| Message | Direction | Notes |
|---------|-----------|-------|
| TBD | gossip | Defined in implementation pass |
### `getStats()` / `ready()` / `close()`
Lifecycle helpers.
## P2P
Joins Hyperswarm when `topic` is set.
@@ -1,15 +1,11 @@
# hyper-p2p-encrypted-topic architecture
# Architecture: hyper-p2p-encrypted-topic
**Tier:** scaffold · **Category:** `trust-security`
## Wire messages
## Role
| type | fields | direction | behavior |
|------|--------|-----------|----------|
| `topic-register` | topicId, keyHint | gossip | Register |
Encrypted topic wrapper.
## State
## Composition
Uses `../../_shared/p2p-bare.js` for Hyperswarm + Protomux when implemented. Does **not** duplicate Holepunch core storage/transport.
## Holepunch boundary
Inspiration: n/a
In-memory structures; gossip via `initModuleSwarm` / `gossipSend` when P2P enabled.
@@ -1,8 +1,8 @@
require('bare-process/global')
const { HyperP2PEncryptedTopic } = require('../index.js')
async function main () {
const m = new HyperP2PEncryptedTopic()
console.log('[scaffold]', m.getStats())
console.log(m.getStats())
await m.close()
}
main().catch(console.error)
@@ -1,60 +1,105 @@
require('bare-process/global')
const EventEmitter = require('bare-events')
const b4a = require('b4a')
const crypto = require('hypercore-crypto')
const { assertNonEmpty } = require('../../_shared/lib/errors.js')
const { attachGossip, sendGossip } = require('../../_shared/storage-gossip-base.js')
const { initModuleSwarm, gossipSend } = require('../../_shared/p2p-bare.js')
const PROTOCOL = 'encrypted-topic/v1'
function deriveKey (hint) {
return crypto.hash(b4a.from(String(hint)))
}
function xorCrypt (keyBuf, dataBuf) {
const out = b4a.alloc(dataBuf.length)
for (let i = 0; i < dataBuf.length; i++) {
out[i] = dataBuf[i] ^ keyBuf[i % keyBuf.length]
}
return out
}
class HyperP2PEncryptedTopic extends EventEmitter {
constructor (opts = {}) {
super()
this.topic = opts.topic || null
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
this._store = new Map()
this._stats = { ops: 0, gossipIn: 0, gossipOut: 0 }
this.keyPair = opts.keyPair || crypto.keyPair()
this._topics = new Map()
this._stats = { registered: 0, encrypted: 0, decrypted: 0, gossipIn: 0, gossipOut: 0 }
this.swarm = null
this._peerMsgs = null
}
put (key, value) {
assertNonEmpty(key, 'key')
this._store.set(key, value)
this._stats.ops++
sendGossip(this, { type: 'encrypted-topic-sync', key, value })
this.emit('update', { key, value })
return true
registerTopic (topicId, keyHint) {
assertNonEmpty(topicId, 'topicId')
assertNonEmpty(keyHint, 'keyHint')
const entry = { topicId, keyHint, key: deriveKey(keyHint), registeredAt: Date.now() }
this._topics.set(topicId, entry)
this._stats.registered++
this._gossip({ type: 'topic-register', topicId, keyHint })
this.emit('registered', { topicId, keyHint })
return entry
}
get (key) { return this._store.get(key) }
delete (key) {
const ok = this._store.delete(key)
if (ok) sendGossip(this, { type: 'encrypted-topic-sync', key, value: null })
return ok
encryptPayload (topicId, data) {
assertNonEmpty(topicId, 'topicId')
const entry = this._topics.get(topicId)
if (!entry) throw new Error(`topic not registered: ${topicId}`)
const plain = b4a.isBuffer(data) ? data : b4a.from(data)
const cipher = xorCrypt(entry.key, plain)
this._stats.encrypted++
return cipher
}
entries () { return [...this._store.entries()] }
decryptPayload (topicId, buf) {
assertNonEmpty(topicId, 'topicId')
if (!b4a.isBuffer(buf)) throw new Error('buf must be a buffer')
const entry = this._topics.get(topicId)
if (!entry) throw new Error(`topic not registered: ${topicId}`)
const plain = xorCrypt(entry.key, buf)
this._stats.decrypted++
return plain
}
_gossip (data) {
if (!this._peerMsgs) return
gossipSend(this, data)
this._stats.gossipOut++
}
_onGossip (d) {
if (!d || d.type !== 'encrypted-topic-sync') return
if (!d || d.type !== 'topic-register' || !d.topicId) return
this._stats.gossipIn++
if (d.key !== undefined) {
if (d.value === null) this._store.delete(d.key)
else this._store.set(d.key, d.value)
if (!this._topics.has(d.topicId)) {
this._topics.set(d.topicId, {
topicId: d.topicId,
keyHint: d.keyHint,
key: deriveKey(d.keyHint),
registeredAt: Date.now()
})
}
}
getStats () { return { ...this._stats, size: this._store.size, protocol: PROTOCOL } }
getStats () {
return { ...this._stats, topics: this._topics.size, protocol: PROTOCOL }
}
async ready () {
if (this.swarm || !this.topic) return this
await attachGossip(this, { keyPair: this.keyPair, topic: this.topic, protocol: PROTOCOL, onmessage: (d) => this._onGossip(d) })
await initModuleSwarm(this, {
keyPair: this.keyPair,
topic: this.topic,
protocol: PROTOCOL,
onmessage: (d) => this._onGossip(d)
})
return this
}
async close () {
if (this.swarm) await this.swarm.destroy().catch(() => {})
this.swarm = null
this.emit('closed')
}
}
module.exports = { HyperP2PEncryptedTopic, PROTOCOL }
module.exports = { HyperP2PEncryptedTopic, PROTOCOL, deriveKey, xorCrypt }
@@ -1,26 +1,39 @@
require('bare-process/global')
const test = require('brittle')
const b4a = require('b4a')
const { HyperP2PEncryptedTopic, PROTOCOL } = require('../index.js')
test('exports', (t) => {
t.ok(HyperP2PEncryptedTopic)
t.ok(PROTOCOL)
t.is(PROTOCOL, 'encrypted-topic/v1')
})
test('basic operation', async (t) => {
test('encrypt decrypt roundtrip', async (t) => {
const m = new HyperP2PEncryptedTopic()
m.put('k', 1); t.is(m.get('k'), 1)
m.registerTopic('secret', 'hint-abc')
const plain = b4a.from('hello peers')
const cipher = m.encryptPayload('secret', plain)
const out = m.decryptPayload('secret', cipher)
t.alike(out, plain)
await m.close()
})
test('unregistered topic throws', async (t) => {
const m = new HyperP2PEncryptedTopic()
try { m.encryptPayload('nope', 'x') } catch (e) { t.ok(e) }
await m.close()
})
test('validation', async (t) => {
const m = new HyperP2PEncryptedTopic()
try { m.put(null, 1) } catch (e) { t.ok(e) }
try { m.registerTopic(null, 'h') } catch (e) { t.ok(e) }
await m.close()
})
test('getStats', async (t) => {
const m = new HyperP2PEncryptedTopic()
t.ok(m.getStats().protocol)
m.registerTopic('t', 'h')
m.encryptPayload('t', 'a')
t.is(m.getStats().encrypted, 1)
await m.close()
})