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
- Pass the exact ID emitted when the receipt was created: `sha256:` plus exactly 64 lowercase hex characters
- Normalize before calling: lowercase the value, prepend `sha256:` if missing, and assert the hex part is 64 chars
- 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
- Persist receipt IDs exactly as emitted (sha256: + 64 lowercase hex)
- Validate IDs at the system boundary (CLI args, HTTP params) with the same regex
- Avoid pipelines that uppercase, trim, or truncate hash strings
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
- invalid anchor task id at index
- actualUsd must be a non-negative finite number
- Agent config must include id, name, and type
- agent must be an object
- anchor task has no query
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)