ruvnet/ruflo · error · Error

invalid receipt ID

Error message

invalid receipt ID

What it means

Flywheel evaluation receipts are identified by their content hash. Every function that turns a receipt ID into a filename (`receiptPath`, used by read/consume/verify APIs) first validates it against `^sha256:[a-f0-9]{64}$`. The check is both a format contract and a path-injection guard — a malformed ID can never reach `path.join` and become a traversal vector.

Solutions

  1. Pass the exact ID emitted when the receipt was created: `sha256:` plus exactly 64 lowercase hex characters
  2. Normalize before calling: lowercase the value, prepend `sha256:` if missing, and assert the hex part is 64 chars
  3. Validate user-supplied IDs with the same regex before invoking the API

Example fix

// before: bare digest from your own records
const receipt = readFlywheelReceipt(root, '6096e48ef8f2182e0f00348a953f0f00fe0415575b300234fe2316f37b768200');

// after: normalized to the required sha256:… form
const id = raw.toLowerCase().startsWith('sha256:') ? raw.toLowerCase() : `sha256:${raw.toLowerCase()}`;
if (!/^sha256:[a-f0-9]{64}$/.test(id)) throw new Error(`not a receipt id: ${raw}`);
const receipt = readFlywheelReceipt(root, id);
Defensive patterns

Strategy: type-guard

Validate before calling

const RECEIPT_ID_RE = /^sha256:[a-f0-9]{64}$/;

function toReceiptId(raw: string): string {
  const normalized = `sha256:${raw.trim().toLowerCase().replace(/^sha256:/, '')}`;
  if (!RECEIPT_ID_RE.test(normalized)) {
    throw new Error(`not a flywheel receipt id: ${raw}`);
  }
  return normalized;
}

Type guard

const RECEIPT_ID_RE = /^sha256:[a-f0-9]{64}$/;

function isReceiptId(value: unknown): value is `sha256:${string}` {
  return typeof value === 'string' && RECEIPT_ID_RE.test(value);
}

// Usage: narrows before the call
if (!isReceiptId(maybeId)) throw new Error(`invalid receipt id: ${String(maybeId)}`);
const receipt = readFlywheelReceipt(root, maybeId);

Try / catch

try {
  return readFlywheelReceipt(root, id);
} catch (e) {
  if (e?.message === 'invalid receipt ID') {
    // normalize once (lowercase + sha256: prefix) and retry a single time
    const normalized = `sha256:${id.trim().toLowerCase().replace(/^sha256:/, '')}`;
    return /^sha256:[a-f0-9]{64}$/.test(normalized) ? readFlywheelReceipt(root, normalized) : null;
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a hash without the `sha256:` prefix, uppercase hex, a truncated or doubly-long hash, or a free-form string (a filename, user input) into `readFlywheelReceipt` or any API taking a receiptId.

Common situations: Hand-copied IDs losing the prefix; a database or log pipeline that uppercases or trims values; forwarding unvalidated CLI arguments as receipt IDs; storing receipts keyed by bare hex digest.

Related errors


AI-assisted analysis of ruvnet/ruflo@5234333c34 (2026-08-18). Data as JSON: /api/errors/c5d6d7b9bb68969b. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/services/flywheel-transaction.ts:272

        try { fs.unlinkSync(lock); } catch { /* lock already gone */ }
      }
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
      try {
        const stat = fs.lstatSync(lock);
        if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
          fs.unlinkSync(lock);
          continue;
        }
      } catch { /* raced with owner */ }
      if (Date.now() >= deadline) throw new Error('timed out acquiring flywheel transaction lock');
      await delay(5);
    }
  }
}

function validateReceiptId(receiptId: string): void {
  if (!/^sha256:[a-f0-9]{64}$/.test(receiptId)) throw new Error('invalid receipt ID');
}

function receiptPath(root: string, receiptId: string): string {
  validateReceiptId(receiptId);
  return path.join(receiptDir(root), `${receiptId.slice('sha256:'.length)}.json`);
}

export function readFlywheelReceipt(root: string, receiptId: string): FlywheelEvaluationReceipt | null {
  try {
    const file = receiptPath(root, receiptId);
    assertSafeFile(file);
    return JSON.parse(fs.readFileSync(file, 'utf8')) as FlywheelEvaluationReceipt;
  } catch {
    return null;
  }
}

/**

View on GitHub (pinned to 5234333c34)