OverviewFilesCommitsIssuesDocsReleasesPeers

autobase-doctor / lib / scanner.js

117 lines · javascript ref:
/**
 * 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
}