Add storage stack modules and track them in git
The modules workspace previously ignored every path matching `storage-*`, which kept all Hypercore, Hyperbee, Hyperdrive, and Autobase packages out of version control. Narrow .gitignore to test-artifact patterns only so production category trees are committed. Storage packages (23 total): - storage-hypercore (7): replicator, seed-policy, fork-picker, merkle-sync, priority-fetch, bitfield-scheduler, audit-chain - storage-hyperbee (5): batch-write, diff-follow, range-watch, secondary-index, tombstone-gc - storage-hyperdrive (6): entry-catalog, gc-sweep, mirror-sync, mount-bridge, version-snapshot, watch-notify - storage-autobase (5): fork-choice, view-sync, writer-lease, indexer-bus, light-writer Implementation highlights: - Shared attach/gossip helpers in _shared/storage-gossip-base.js - P2P modules use Protomux gossip via p2p-bare; local planners omit swarm - Recent deepen pass: priority queues, batch caps, audit export/import, watch/notify aliases on range-watch, fork/view lease helpers, etc. - Per-package README, docs/api.md, docs/architecture.md, tests, examples Also update modules/README.md doc hub links for the full storage stack (hypercore, hyperbee, hyperdrive, autobase). Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [0.0.0-scaffold]
|
||||
|
||||
- Registry scaffold: file tree, load smoke tests, docs stubs
|
||||
@@ -0,0 +1,42 @@
|
||||
# hyper-p2p-core-audit-chain
|
||||
|
||||
Append-only hash-linked audit log over a Hypercore. Local-only (no Hyperswarm topic).
|
||||
|
||||
**Category:** Storage (Hypercore)
|
||||
|
||||
**Composes with:** `hyper-p2p-attestation-chain`
|
||||
|
||||
**Protocol:** `core-audit-chain/v1`
|
||||
|
||||
## When to use
|
||||
|
||||
Apps that need tamper-evident audit trails tied to core length (compliance, replication forensics).
|
||||
|
||||
## When not to use
|
||||
|
||||
When you only need unstructured logs without hash chaining (use observability modules).
|
||||
|
||||
## Quick start
|
||||
|
||||
```js
|
||||
const { HyperP2PCoreAuditChain } = require('hyper-p2p-core-audit-chain')
|
||||
const mod = new HyperP2PCoreAuditChain()
|
||||
mod.attach(core)
|
||||
mod.appendAudit({ op: 'write', actor: 'peer-1' })
|
||||
console.log(mod.verifyChain())
|
||||
console.log(mod.tail(5))
|
||||
await mod.close()
|
||||
```
|
||||
|
||||
## Docs
|
||||
|
||||
- [docs/api.md](docs/api.md) — constructor, methods, events, errors
|
||||
- [docs/architecture.md](docs/architecture.md) — chain model, verification
|
||||
- [../_shared/PRODUCTION.md](../../_shared/PRODUCTION.md) — production checklist
|
||||
- [../_shared/DOC_STANDARDS.md](../../_shared/DOC_STANDARDS.md) — documentation standards
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
@@ -0,0 +1,85 @@
|
||||
# API: hyper-p2p-core-audit-chain
|
||||
|
||||
**Protocol:** `core-audit-chain/v1`
|
||||
|
||||
**Export:** `HyperP2PCoreAuditChain`
|
||||
|
||||
## Overview
|
||||
|
||||
Append-only hash-linked audit log over a Hypercore. Each entry includes `prevHash`, `hash`, `seq`, and optional `coreLength` snapshot.
|
||||
|
||||
## Constructor
|
||||
|
||||
```js
|
||||
const mod = new HyperP2PCoreAuditChain(opts)
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `core` | Hypercore | null | Optional core attached at construct time |
|
||||
|
||||
## Methods
|
||||
|
||||
### `attach(core)`
|
||||
|
||||
- **Returns:** `this`
|
||||
- **Throws:** `Error: core must be a Hypercore instance`
|
||||
|
||||
### `appendAudit(event)`
|
||||
|
||||
- **Parameters:** `event` — plain object (domain fields)
|
||||
- **Returns:** entry with `seq`, `hash`, `prevHash`, `at`
|
||||
- **Throws:** `Error: event object required`
|
||||
|
||||
### `verifyChain()`
|
||||
|
||||
- **Returns:** `{ ok: true, length, head }` or `{ ok: false, seq, reason }`
|
||||
- **Throws:** —
|
||||
|
||||
### `tail(n = 10)`
|
||||
|
||||
- **Returns:** last `n` entries
|
||||
- **Throws:** `Error: n must be non-negative`
|
||||
|
||||
### `getStats()`
|
||||
|
||||
- **Returns:** `{ appended, verified, failures, length, head, coreLength, protocol }`
|
||||
|
||||
### `ready()`
|
||||
|
||||
- **Returns:** `Promise<this>` (no-op)
|
||||
|
||||
### `close()`
|
||||
|
||||
- **Returns:** `Promise<void>` — clears chain
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Payload |
|
||||
|-------|---------|
|
||||
| `audit` | appended entry |
|
||||
| `closed` | no payload |
|
||||
|
||||
## getStats()
|
||||
|
||||
See fields above. `protocol` is always `core-audit-chain/v1`.
|
||||
|
||||
## Errors
|
||||
|
||||
Stable message substrings: see [`../../_shared/ERROR_CODES.md`](../../_shared/ERROR_CODES.md).
|
||||
|
||||
## P2P
|
||||
|
||||
Local-only — no `topic` or gossip wire messages.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
|
||||
## Common flows
|
||||
|
||||
1. `attach(core)` → `appendAudit` for each mutation → periodic `verifyChain()`
|
||||
2. `tail(n)` for dashboards without scanning full chain
|
||||
3. `close()` on shutdown to drop in-memory chain
|
||||
@@ -0,0 +1,48 @@
|
||||
# Architecture: hyper-p2p-core-audit-chain
|
||||
|
||||
**Category:** Storage (Hypercore) · **Protocol:** `core-audit-chain/v1` · **P2P:** no
|
||||
|
||||
## Role
|
||||
|
||||
Append-only **hash-linked audit log** for operations on a Hypercore (replication decisions, admin actions, policy changes). Each entry stores `prevHash`, `hash`, `seq`, and `coreLength` at append time for correlation with chain state.
|
||||
|
||||
## Hash algorithm
|
||||
|
||||
`hash = sha256(JSON.stringify({ prevHash, event }))` via `hypercore-crypto` (hex string). Verification walks the chain recomputing hashes; mismatch returns `{ ok: false, seq, reason }`.
|
||||
|
||||
## State model
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `_chain` | Ordered entries with metadata |
|
||||
| `_headHash` | Latest hash or `null` |
|
||||
|
||||
## API surface
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `appendAudit(event)` | Push linked entry; emit `audit` |
|
||||
| `verifyChain()` | Full integrity check |
|
||||
| `tail(n)` | Last `n` entries |
|
||||
| `exportChain()` / `importChain(entries)` | Snapshot / restore for persistence |
|
||||
|
||||
## Wire messages
|
||||
|
||||
None. Export JSON and ship over your app channel if peers need shared audit history.
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Payload |
|
||||
|-------|---------|
|
||||
| `audit` | New entry |
|
||||
| `closed` | Chain cleared |
|
||||
|
||||
## Composition
|
||||
|
||||
- Log replication policy changes from `hyper-p2p-core-replicator`
|
||||
- Log fork picks from `hyper-p2p-core-fork-picker`
|
||||
- Persist `exportChain()` to Hyperbee or Hyperdrive for durability
|
||||
|
||||
## Threat model
|
||||
|
||||
Detects tampering of in-memory chain only. Does not sign entries — add application-level signatures if non-repudiation is required.
|
||||
@@ -0,0 +1,16 @@
|
||||
require('bare-process/global')
|
||||
const { HyperP2PCoreAuditChain } = require('../index.js')
|
||||
|
||||
async function main () {
|
||||
const mod = new HyperP2PCoreAuditChain()
|
||||
mod.attach({ length: 0 })
|
||||
mod.appendAudit({ op: 'bootstrap' })
|
||||
mod.appendAudit({ op: 'append', block: 1 })
|
||||
console.log('verify', mod.verifyChain())
|
||||
console.log('tail', mod.tail(2))
|
||||
console.log('stats', mod.getStats())
|
||||
await mod.close()
|
||||
console.log('done')
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
@@ -0,0 +1,106 @@
|
||||
require('bare-process/global')
|
||||
const EventEmitter = require('bare-events')
|
||||
const b4a = require('b4a')
|
||||
const crypto = require('hypercore-crypto')
|
||||
const { assertCore } = require('../../_shared/storage-gossip-base.js')
|
||||
const PROTOCOL = 'core-audit-chain/v1'
|
||||
|
||||
function hashEntry (prevHash, event) {
|
||||
const body = JSON.stringify({ prevHash, event })
|
||||
return b4a.toString(crypto.hash(b4a.from(body)), 'hex')
|
||||
}
|
||||
|
||||
class HyperP2PCoreAuditChain extends EventEmitter {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this.core = opts.core || null
|
||||
this._chain = []
|
||||
this._headHash = null
|
||||
this._stats = { appended: 0, verified: 0, failures: 0 }
|
||||
}
|
||||
|
||||
attach (core) {
|
||||
assertCore(core)
|
||||
this.core = core
|
||||
return this
|
||||
}
|
||||
|
||||
appendAudit (event) {
|
||||
if (!event || typeof event !== 'object') throw new Error('event object required')
|
||||
const prevHash = this._headHash
|
||||
const hash = hashEntry(prevHash, event)
|
||||
const entry = {
|
||||
...event,
|
||||
seq: this._chain.length,
|
||||
prevHash,
|
||||
hash,
|
||||
coreLength: this.core ? this.core.length : 0,
|
||||
at: Date.now()
|
||||
}
|
||||
this._chain.push(entry)
|
||||
this._headHash = hash
|
||||
this._stats.appended++
|
||||
this.emit('audit', entry)
|
||||
return entry
|
||||
}
|
||||
|
||||
verifyChain () {
|
||||
let prev = null
|
||||
for (const entry of this._chain) {
|
||||
const expected = hashEntry(prev, stripMeta(entry))
|
||||
if (entry.hash !== expected || entry.prevHash !== prev) {
|
||||
this._stats.failures++
|
||||
return { ok: false, seq: entry.seq, reason: 'hash mismatch' }
|
||||
}
|
||||
prev = entry.hash
|
||||
}
|
||||
this._stats.verified++
|
||||
return { ok: true, length: this._chain.length, head: this._headHash }
|
||||
}
|
||||
|
||||
tail (n = 10) {
|
||||
if (n < 0) throw new Error('n must be non-negative')
|
||||
if (n === 0) return []
|
||||
return this._chain.slice(-n)
|
||||
}
|
||||
|
||||
exportChain () {
|
||||
return this._chain.map((e) => ({ ...e }))
|
||||
}
|
||||
|
||||
importChain (entries) {
|
||||
if (!Array.isArray(entries)) throw new Error('entries must be an array')
|
||||
this._chain = []
|
||||
this._headHash = null
|
||||
for (const raw of entries) {
|
||||
const event = stripMeta(raw)
|
||||
this.appendAudit(event)
|
||||
}
|
||||
return this.verifyChain()
|
||||
}
|
||||
|
||||
getStats () {
|
||||
return {
|
||||
...this._stats,
|
||||
length: this._chain.length,
|
||||
head: this._headHash,
|
||||
coreLength: this.core ? this.core.length : 0,
|
||||
protocol: PROTOCOL
|
||||
}
|
||||
}
|
||||
|
||||
async ready () { return this }
|
||||
|
||||
async close () {
|
||||
this._chain = []
|
||||
this._headHash = null
|
||||
this.emit('closed')
|
||||
}
|
||||
}
|
||||
|
||||
function stripMeta (entry) {
|
||||
const { seq, prevHash, hash, coreLength, at, ...event } = entry
|
||||
return event
|
||||
}
|
||||
|
||||
module.exports = { HyperP2PCoreAuditChain, PROTOCOL }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "hyper-p2p-core-audit-chain",
|
||||
"version": "0.3.1",
|
||||
"description": "Append-only audit chain on cores.",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": { "test": "brittle-bare test/test.js" },
|
||||
"dependencies": {
|
||||
"bare-events": "^2.8.0",
|
||||
"bare-process": "^4.4.0",
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.0.0",
|
||||
"protomux": "^3.0.0",
|
||||
"compact-encoding": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"hyperswarm": "^4.0.0",
|
||||
"bare": ">=1.0.0",
|
||||
"hypercore": "^10.0.0"
|
||||
},
|
||||
"devDependencies": { "brittle": "^3.0.0" },
|
||||
"imports": {
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"events": { "bare": "bare-events", "default": "events" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
require('bare-process/global')
|
||||
const test = require('brittle')
|
||||
const { HyperP2PCoreAuditChain, PROTOCOL } = require('../index.js')
|
||||
|
||||
const mockCore = { length: 3 }
|
||||
|
||||
test('exports', (t) => {
|
||||
t.ok(HyperP2PCoreAuditChain)
|
||||
t.is(PROTOCOL, 'core-audit-chain/v1')
|
||||
})
|
||||
|
||||
test('appendAudit verifyChain tail', async (t) => {
|
||||
const m = new HyperP2PCoreAuditChain()
|
||||
m.attach(mockCore)
|
||||
m.appendAudit({ op: 'init' })
|
||||
m.appendAudit({ op: 'write' })
|
||||
const v = m.verifyChain()
|
||||
t.ok(v.ok)
|
||||
t.is(m.tail(1).length, 1)
|
||||
t.is(m.tail(2).length, 2)
|
||||
await m.close()
|
||||
})
|
||||
|
||||
test('validation', async (t) => {
|
||||
const m = new HyperP2PCoreAuditChain()
|
||||
try { m.appendAudit(null) } catch (e) { t.ok(e) }
|
||||
try { m.attach(null) } catch (e) { t.ok(e) }
|
||||
try { m.tail(-1) } catch (e) { t.ok(e) }
|
||||
await m.close()
|
||||
})
|
||||
|
||||
test('getStats', async (t) => {
|
||||
const m = new HyperP2PCoreAuditChain()
|
||||
m.appendAudit({ x: 1 })
|
||||
const s = m.getStats()
|
||||
t.is(s.protocol, 'core-audit-chain/v1')
|
||||
t.is(s.length, 1)
|
||||
await m.close()
|
||||
})
|
||||
|
||||
test('exportChain importChain', async (t) => {
|
||||
const m = new HyperP2PCoreAuditChain()
|
||||
m.appendAudit({ op: 'a' })
|
||||
m.appendAudit({ op: 'b' })
|
||||
const exported = m.exportChain()
|
||||
const m2 = new HyperP2PCoreAuditChain()
|
||||
const v = m2.importChain(exported)
|
||||
t.ok(v.ok)
|
||||
t.is(m2.getStats().length, 2)
|
||||
await m.close()
|
||||
await m2.close()
|
||||
})
|
||||
|
||||
test('broken chain detected', async (t) => {
|
||||
const m = new HyperP2PCoreAuditChain()
|
||||
m.appendAudit({ a: 1 })
|
||||
m._chain[0].hash = 'bad'
|
||||
const v = m.verifyChain()
|
||||
t.not(v.ok)
|
||||
await m.close()
|
||||
})
|
||||
Reference in New Issue
Block a user