Skip to content

v0.2.1 — fix two cap bypasses shipped in 0.2.0

Latest

Choose a tag to compare

@premsreelathasugeendran premsreelathasugeendran released this 29 Jul 22:59

0.2.1 — 2026-07-29

Two cap bypasses shipped in 0.2.0. Upgrade. Both were introduced by the
0.2.0 limiter itself — the release whose entire purpose was to make caps hold —
and both were found by a fact-check pass after publishing, not before.

Fixed

  • Rotating intent.agentId reset the cap. The reservation was scoped to
    agentId, a field this project's own threat model
    (CONTRIBUTING rule 4, types.ts)
    states the attacker controls. Reproduced: two guards sharing one limiter,
    $100 through a $50/day cap, by changing one string. The same test with a
    fixed agentId correctly denied — which is why the conformance suite missed
    it entirely.

    Reservations are now scoped to policy.id, which the operator sets. Note
    this also means the pre-0.2.0 behaviour is restored: the old in-memory history
    deliberately ignored agentId for exactly this reason, and 0.2.0 silently
    regressed a mitigation SECURITY.md had claimed since 0.1.0. If you want
    per-agent budgets, derive the scope from an id you assign to a trusted
    principal, never from the intent.

  • Reusing an intent.id ran the rail again while counting one charge. A
    replayed reservation returned ok and the guard fell through to the executor.
    Reproduced: the same intent id twice ran the rail twice, counted once; a
    throwing executor retried eight times ran the rail eight times and still
    counted one $50 charge. That directly contradicted the burn-on-failure
    guarantee 0.2.0 added, which exists so an executor timing out after the
    charge lands cannot be retried past the cap.

    A replayed reservation now returns status: "replayed"executed if the
    first attempt committed, unresolved if it never settled (money may have
    moved; the guard will not guess). When a mandate is present the mandate's
    consume-once registry still decides, because it holds strictly better
    information: the intent digest, so id reuse with different money is DENIED
    rather than replayed, and the settled outcome, so a failed attempt replays
    failed rather than unresolved.

Changed (breaking, and it is a one-day-old interface)

  • ReserveRequest.agentIdReserveRequest.scope. Any SpendLimiter you
    wrote against 0.2.0 needs this rename, and should key on scope. The
    Postgres schema column agent_idscope with it.
  • ReserveResult gains state on a replay, so the caller can tell "the rail
    ran" from "the rail may have run".
  • GuardResult's replayed variant has mandateId optional — an intent id
    can now be replayed by the limiter with no mandate involved.

Documentation corrections

The same pass found five claims a reader could have disproved:

  • The conformance suites are not published. @vaduno/guard ships only
    dist/, yet both suites' own docblocks told you to
    import … from "@vaduno/guard/test/…", CONTRIBUTING repeated it, and
    @vaduno/postgres@0.2.0's README says they "ship with @vaduno/guard" —
    that one is immutable on npm now. They are repo test files; copy them or send
    a PR.
  • "This project has shipped that bug twice" — written twice, shipped once.
    The maxUses gate was fixed six days before the first publish.
  • "All five packages are versioned together" — six.
  • The changelog claimed two breaking minor bumps; there had been one.

Tests 357 → 364, including regression tests for both bypasses.


npm install @vaduno/guard@0.2.1

Verified before release: 364 tests green on Node 18/20/22/24 across Ubuntu,
Windows and macOS; 40 more against a real Postgres 16 in CI; release gate clean
on all six packages; every demo runs; and the cross-process demo still shows
$100 spent with the per-instance default versus exactly $50 with a shared
limiter.

Still true: zero users, never run in production, no professional security
audit
. @vaduno/stripe has never run against Stripe in any mode. Both bugs
above were found by a fact-check pass against the source, not by an auditor and
not by a user — which is the argument for reading
docs/SECURITY-MODEL.md
before you trust any of it.

Report vulnerabilities via private advisory, not a public issue.