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

  1. Compute expiresAt as Date.now() + ttlMs (milliseconds) so it strictly exceeds issuedAt.
  2. Use an integer maxUses >= 1; if you want unlimited uses, set a large finite cap — zero is invalid.
  3. 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

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


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