/**
* Scanner — find poisoned ops in Autobase writer cores.
*
* Walks every core in a corestore namespace looking for ops that match
* a poison pattern (default: add-writer ops with non-Hypercore keys).
*
* Returns a list of { repoName, coreKey, poisonIndex, totalOps, op }
*/
export const DEFAULT_POISON_MARKER = Buffer.from('"type":"add-writer"')
/**
* Scan a single writer core for poisoned ops.
*
* @param {Corestore} ns - Namespaced corestore (e.g. store.namespace('linked:repo'))
* @param {Object} opts
* @param {Buffer} opts.marker - Binary marker to search for in raw blocks
* @param {Function} opts.isPoisoned - (op) => boolean, validates decoded op
* @returns {{ poisonIndex: number, op: object } | null}
*/
export async function scanWriterCore (ns, opts = {}) {
const {
marker = DEFAULT_POISON_MARKER,
isPoisoned = defaultIsPoisoned
} = opts
const local = ns.get({ name: 'local' })
await local.ready()
if (local.length === 0) {
await local.close()
return null
}
// Open raw session to read actual bytes
const raw = ns.get({ key: local.key, valueEncoding: 'binary' })
await raw.ready()
let result = null
for (let i = 0; i < raw.length; i++) {
try {
const block = await raw.get(i)
if (block.indexOf(marker) === -1) continue
const op = extractJson(block)
if (op && isPoisoned(op)) {
result = { poisonIndex: i, totalOps: raw.length, op }
break
}
} catch {}
}
await raw.close()
await local.close()
return result
}
/**
* Default poison check: any add-writer op with a serverKey field.
* In a healthy system, add-writer ops should use Hypercore local core keys,
* not server identity keys.
*/
function defaultIsPoisoned (op) {
return op.type === 'add-writer' && !!op.serverKey
}
/**
* Extract JSON from an Autobase OplogMessage block.
* The block has compact-encoded headers before the JSON payload.
*/
export function extractJson (block) {
const start = block.indexOf(Buffer.from('{"type":'))
if (start === -1) return null
let end = block.length - 1
while (end > start && block[end] !== 0x7d) end--
if (end <= start) return null
try {
return JSON.parse(block.slice(start, end + 1).toString())
} catch {
return null
}
}
/**
* List all ops in a writer core as decoded JSON objects.
*/
export async function listOps (ns) {
const local = ns.get({ name: 'local' })
await local.ready()
if (local.length === 0) {
await local.close()
return []
}
const raw = ns.get({ key: local.key, valueEncoding: 'binary' })
await raw.ready()
const ops = []
for (let i = 0; i < raw.length; i++) {
try {
const block = await raw.get(i)
const op = extractJson(block)
ops.push({ index: i, op, size: block.length })
} catch (err) {
ops.push({ index: i, op: null, error: err.message })
}
}
await raw.close()
await local.close()
return ops
}