sealforge / src /storage /worm-log.ts
SNAPKITTYWEST's picture
October 2026 main drop: mirror from GitHub
a18e5eb verified
Raw History Blame Contribute Delete
3.2 kB
import * as crypto from 'crypto'
import * as fs from 'fs'
import * as path from 'path'
import { v4 as uuidv4 } from 'uuid'
export interface WormEntry {
id: string
receivedAt: string
type: string
payload: unknown
chainHash: string
}
export class WormLog {
private filePath: string
private secret: string
private chainTip: string = 'GENESIS'
constructor(dataDir: string = './data') {
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
this.filePath = path.join(dataDir, 'sealforge.worm.jsonl')
const secret = process.env.WORM_SECRET
if (!secret || secret.length < 32) throw new Error('[WORM] WORM_SECRET must be set and at least 32 characters')
this.secret = secret
this.loadChainTip()
}
private loadChainTip(): void {
if (!fs.existsSync(this.filePath)) return
const lines = fs.readFileSync(this.filePath, 'utf8').trim().split('\n').filter(Boolean)
if (lines.length > 0) {
const last = JSON.parse(lines[lines.length - 1]) as WormEntry
this.chainTip = last.chainHash
console.log(`[WORM] Chain loaded — ${lines.length} entries — tip: ${this.chainTip.slice(0, 16)}...`)
}
}
private computeHash(id: string, receivedAt: string, bodyRaw: string): string {
return crypto
.createHmac('sha256', this.secret)
.update(`${this.chainTip}:${id}:${receivedAt}:${bodyRaw}`)
.digest('hex')
}
append(type: string, payload: unknown): WormEntry {
const id = uuidv4()
const receivedAt = new Date().toISOString()
const bodyRaw = JSON.stringify(payload)
const chainHash = this.computeHash(id, receivedAt, bodyRaw)
const entry: WormEntry = { id, receivedAt, type, payload, chainHash }
fs.appendFileSync(this.filePath, JSON.stringify(entry) + '\n')
this.chainTip = chainHash
return entry
}
getAll(limit = 100, offset = 0): { entries: WormEntry[]; chainValid: boolean } {
if (!fs.existsSync(this.filePath)) return { entries: [], chainValid: true }
const lines = fs.readFileSync(this.filePath, 'utf8').trim().split('\n').filter(Boolean)
const entries = lines.slice(offset, offset + limit).map(l => JSON.parse(l) as WormEntry)
return { entries, chainValid: this.verifyChain() }
}
getById(id: string): WormEntry | null {
if (!fs.existsSync(this.filePath)) return null
const lines = fs.readFileSync(this.filePath, 'utf8').trim().split('\n').filter(Boolean)
for (const line of lines) {
const entry = JSON.parse(line) as WormEntry
if (entry.id === id) return entry
}
return null
}
verifyChain(): boolean {
if (!fs.existsSync(this.filePath)) return true
const lines = fs.readFileSync(this.filePath, 'utf8').trim().split('\n').filter(Boolean)
let prev = 'GENESIS'
for (const line of lines) {
const entry = JSON.parse(line) as WormEntry
const expected = crypto
.createHmac('sha256', this.secret)
.update(`${prev}:${entry.id}:${entry.receivedAt}:${JSON.stringify(entry.payload)}`)
.digest('hex')
if (expected !== entry.chainHash) return false
prev = entry.chainHash
}
return true
}
tip(): string {
return this.chainTip
}
}