ruvnet/ruflo · error · Error
invalid-approval
Error message
invalid-approval
What it means
The final shape check in issueApproval(): after issuer checks pass, the record must have a truthy id, expiresAt strictly greater than issuedAt, an integer maxUses >= 1, and an integer uses in [0, maxUses]. Anything else throws Error('invalid-approval'). Note issuedAt defaults to engine now() when omitted.
Source
Thrown at v3/@claude-flow/security/src/policy/engine.ts:119
if (index >= 0) this.state.budgets[index] = structuredClone(limit);
else this.state.budgets.push(structuredClone(limit));
}
issueApproval(approval: Omit<PolicyApproval, 'uses' | 'issuedAt'> & { uses?: number; issuedAt?: number }): PolicyApproval {
if (approval.issuedBy === approval.principal) throw new Error('self-approval-forbidden');
if (this.approvalIssuerVerifier?.(approval.issuedBy) !== true) {
throw new Error('untrusted-approval-issuer');
}
const issuedAt = approval.issuedAt ?? this.now();
const record: PolicyApproval = { ...approval, issuedAt, uses: approval.uses ?? 0 };
if (this.state.approvals.some((item) => item.id === record.id)) throw new Error('duplicate-approval-id');
if (!record.id
|| record.expiresAt <= issuedAt
|| !Number.isInteger(record.maxUses)
|| record.maxUses <= 0
|| !Number.isInteger(record.uses)
|| record.uses < 0
|| record.uses > record.maxUses) throw new Error('invalid-approval');
this.state.approvals.push(record);
return structuredClone(record);
}
revokeApproval(id: string): boolean {
const approval = this.state.approvals.find((item) => item.id === id);
if (!approval || approval.revokedAt) return false;
approval.revokedAt = this.now();
return true;
}
evaluate(request: PolicyRequest): PolicyDecision {
const normalized: PolicyRequest = {
...request,
requestId: request.requestId ?? crypto.randomUUID(),
// Caller time is evidence only; expiry, budgets, and receipts always use
// the authority's clock.
context: { ...request.context, now: this.now() },View on GitHub (pinned to fa13ee4ad6)
Solutions
- Compute expiresAt as Date.now() + ttlMs (milliseconds) so it strictly exceeds issuedAt.
- Use an integer maxUses >= 1; if you want unlimited uses, set a large finite cap — zero is invalid.
- Validate persisted approval records (integer counters, expiry ordering) before re-issuing them into the engine.
Example fix
// before
engine.issueApproval({ id, issuedBy, principal, expiresAt: 1893456000, maxUses: 0, ... });
// after
engine.issueApproval({
id,
issuedBy,
principal,
expiresAt: Date.now() + 60 * 60_000,
maxUses: 10,
...,
}); Defensive patterns
Strategy: validation
Validate before calling
function validApproval(a: { id?: string; expiresAt?: number; maxUses?: number; uses?: number }): boolean {
return !!a.id
&& Number.isInteger(a.maxUses) && a.maxUses > 0
&& Number.isInteger(a.uses) && a.uses >= 0 && a.uses <= a.maxUses
&& a.expiresAt > Date.now();
}
if (!validApproval(approval)) throw new Error('approval record failed pre-check'); Type guard
function isWellFormedApproval(a: unknown): a is Omit<PolicyApproval, 'uses' | 'issuedAt'> {
const rec = a as any;
return typeof rec?.id === 'string' && rec.id.length > 0
&& Number.isInteger(rec.maxUses) && rec.maxUses > 0
&& Number.isInteger(rec.expiresAt) && rec.expiresAt > Date.now();
} Try / catch
try {
return engine.issueApproval(approval);
} catch (err) {
if (err instanceof Error && err.message === 'invalid-approval') {
throw new BadRequest('approval needs id, future expiresAt (ms), integer maxUses>=1, 0<=uses<=maxUses');
}
throw err;
} Prevention
- Always compute expiresAt as Date.now() + ttlMs in milliseconds — never seconds.
- Treat maxUses as a required positive integer; 'unlimited' is a large finite cap, not 0.
- Validate approvals rehydrated from persistence (counters/expiry) before re-issuing into a fresh engine.
When it happens
Trigger: expiresAt supplied in seconds while issuedAt is epoch milliseconds (looks already-expired); expiresAt === issuedAt (TTL of zero); maxUses: 0 used to mean 'unlimited'; uses > maxUses when replaying a heavily-consumed approval into a new engine.
Common situations: Mixing seconds/milliseconds timestamps between systems; rehydrating approvals from persistence where counters were corrupted; hand-built approval fixtures in tests with placeholder zeros.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- mode must be legacy, observe, or enforce
- Policy canonicalization rejects non-finite numbers
- invalid-policy-rule
- invalid-policy-rule-limit
- invalid-budget-period
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/88e8c48825ec9e23.
Report an issue: GitHub.