ruvnet/ruflo · error · TypeError

Policy canonicalization rejects non-finite numbers

Error message

Policy canonicalization rejects non-finite numbers

What it means

canonicalizePolicy() recursively normalizes a policy object (dropping undefined entries, sorting keys) to produce a deterministic JSON string used for policy hashing. While walking values it throws TypeError on any NaN/Infinity/-Infinity, because JSON.stringify would silently coerce those to null and two different policies could then hash identically.

Source

Thrown at v3/@claude-flow/security/src/policy/canonical.ts:14

import { createHash, createHmac, timingSafeEqual } from 'node:crypto';

function normalize(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(normalize);
  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value as Record<string, unknown>)
        .filter(([, item]) => item !== undefined)
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([key, item]) => [key, normalize(item)]),
    );
  }
  if (typeof value === 'number' && !Number.isFinite(value)) {
    throw new TypeError('Policy canonicalization rejects non-finite numbers');
  }
  return value;
}

export function canonicalizePolicy(value: unknown): string {
  return JSON.stringify(normalize(value));
}

export function policyHash(value: unknown): string {
  return `sha256:${createHash('sha256').update(canonicalizePolicy(value)).digest('hex')}`;
}

export function signPolicyHash(hash: string, key: string | Buffer): string {
  return createHmac('sha256', key).update(hash).digest('base64url');
}

export function verifyPolicySignature(hash: string, signature: string, key: string | Buffer): boolean {
  const expected = Buffer.from(signPolicyHash(hash, key));

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Find the offending key: log JSON.stringify of the policy with a replacer that flags non-finite numbers before hashing.
  2. Fix the upstream computation so it yields a finite number, or omit the key entirely (undefined entries are filtered out during normalization).
  3. Sanitize numeric config at load time: coerce NaN/Infinity to undefined and validate ranges.

Example fix

// before
const maxCost = Number(process.env.MAX_COST_USD); // NaN when unset
const hash = policyHash({ rules: [{ constraints: { maxCostUsd: maxCost } }] });

// after
const raw = Number(process.env.MAX_COST_USD);
const maxCost = Number.isFinite(raw) ? raw : undefined;
const hash = policyHash({ rules: [{ constraints: { maxCostUsd: maxCost } }] });
Defensive patterns

Strategy: validation

Validate before calling

function assertFiniteTree(value: unknown): void {
  if (typeof value === 'number' && !Number.isFinite(value)) {
    throw new TypeError(`non-finite number at path: ${pathOf(value)}`);
  }
  if (Array.isArray(value)) return value.forEach(assertFiniteTree);
  if (value && typeof value === 'object') {
    for (const v of Object.values(value)) assertFiniteTree(v);
  }
}
assertFiniteTree(policy); // before policyHash(policy)

Type guard

function isFiniteNumber(v: unknown): v is number {
  return typeof v === 'number' && Number.isFinite(v);
}

Try / catch

try {
  return policyHash(policy);
} catch (err) {
  if (err instanceof TypeError && /non-finite/.test(err.message)) {
    logger.error('policy contains NaN/Infinity — check numeric config sources');
  }
  throw err;
}

Prevention

When it happens

Trigger: policyHash({ limit: NaN }) after Number('abc') or parseFloat of a non-numeric env value; { cost: Infinity } from dividing by zero; any nested rule object containing a computed number that degenerated to NaN.

Common situations: Numeric limits parsed from environment variables or CLI flags that are unset or misspelled; arithmetic on undefined values (undefined * 2 === NaN); JSON revivers that produce non-finite results.

Related errors


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