npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@projectx-social/policy

v1.0.2

Published

A pure evaluator with **zero dependencies and no I/O**. It takes what a simulation observed, what an operator wrote down, and what the agent has already spent, and returns allow, or a reason.

Readme

@projectx-social/policy

A pure evaluator with zero dependencies and no I/O. It takes what a simulation observed, what an operator wrote down, and what the agent has already spent, and returns allow, or a reason.

evaluate(simulatedEffects, policyDoc, ledgerState)
//  -> { allow: true }
//  |  { allow: false, reason: string, ruleId: RuleId }

It signs nothing, reads nothing, and remembers nothing. @projectx-social/signer is its only caller that matters, and it is the only place in this repository where a signature is produced.


Zero dependencies is a security property, not a preference

This package is the last thing that says no before a signature exists. Every line it runs must be a line in this repository. A transitive dependency here would be code with a vote on whether an agent may spend, arriving through a lockfile bump nobody read.

It also performs no I/O and reads no clock. The current time is an input (LedgerState.nowMs), so an evaluation is a pure function of its three arguments — which means a decision recorded in an audit trail can be replayed years later and must reach the same verdict. A rule that read Date.now() internally could not be replayed at all.


Every rule is mutation-tested

A policy engine whose rules have never been shown to change an outcome is decoration, and decoration that is trusted is worse than nothing.

test/mutation.test.ts therefore does two things for every rule: it asserts the full rule set refuses a transaction that violates exactly that rule and names that rule, then deletes the rule and asserts the remaining rules now permit the same transaction. Step two is the one that matters — without it, a rule sitting behind another that happened to fire first would look tested.

Each fixture violates exactly one rule. A fixture that violated two would leave the other still refusing after a deletion, and the mutation test would report a passing rule that had in fact been removed. A test that cannot fail is not a test.

If a rule is added without a fixture, covers every rule exactly once fails the build. An untested rule cannot enter the list quietly.

| Rule | What it refuses | Proven by deleting it | |---|---|---| | policy-version | a document from a schema this evaluator does not understand | ✅ | | sender-mismatch | a simulation belonging to a different address than the policy governs | ✅ | | command-kind | a Publish, an Upgrade, or any kind with no rule of its own | ✅ | | move-call-target | claim_earnings called by an agent authorised only to buy | ✅ | | type-argument | an allowed function instantiated at a coin type never permitted | ✅ | | transfer-recipient | the purchased object transferred to a stranger instead of home | ✅ | | object-input | an attacker's CreatorVault substituted for the one the principal authorised | ✅ | | gas-budget | a gas budget above the ceiling | ✅ | | balance-evidence | a simulation whose balance changes were never requested | ✅ | | amount-wellformed | an amount BigInt would silently read as zero | ✅ | | coin-type-unlisted | an outflow in a coin type with no configured ceiling | ✅ | | outflow-ceiling | a spend that fits alone but breaches the rolling window total | ✅ | | approval-threshold | a spend inside the ceiling but above what may go out unattended | ✅ |


Everything is an allow-list, and there is no wildcard

There is no "*", no allowAll, and no way to express "any target" or "any recipient". That is the most important decision in this package and it is worth the inconvenience.

The scenario this exists for is an agent choosing what to buy from text somebody else wrote. Prompt injection does not need a new capability — it only needs one the operator left open because narrowing it was tedious. A deny-list asks the author to enumerate the attacks; an allow-list asks them to enumerate the job. Only one of those is a list an author can finish.

The concrete case on this protocol: an agent authorised to buy must not be able to call creator::claim_earnings. Both live in the same module of the same package, both are public fun, and nothing on chain distinguishes them. allowedTargets is where that distinction is made, and a wildcard would erase it in one character.

The noun, not just the verb

allowedTargets bounds which function runs; allowedObjects bounds which objects it runs on, and until that list existed the second question had no answer at all. creator::unlock takes vault: &mut CreatorVault<T> (sui-contracts/sources/creator.move:661), and that one argument decides whose earnings the payment lands in. Swap it and every other rule still passes: the permitted target, the permitted coin type, the Unlock transferred home to the agent, the spend inside the rolling ceiling. Vault creation is open to anyone for 29 SUI, so the destination is attacker-supplied, cheap and repeatable — a ceiling caps one drain, it does not stop one.

Refusing shared objects wholesale is not the fix: unlock also takes the Platform and the Clock, both shared and both mandatory, so that rule would deny the call it exists to permit and be switched off within a day. The discrimination is by id. A policy author writes four to six: the vault ids the agent may buy from, the Platform, the Clock (0x6 — the short spelling is normalised against the padded id the node reports), and the agent's own SocialAccount, whose id is stable because account.move:174 transfers it with key and no store.

The honest limit: a Coin object's id changes with every split and merge, so an agent paying from a discrete owned coin will be refused here every time, naming an id nobody could have listed. Pay by splitting the gas coin instead — its result is a command result, never an input.

There is also no allowWithWarning, no override and no force. A caller who wants a different answer must change the policy document — which is hashed into every audit entry, so the widening is visible afterwards at the exact entry where it first took effect. A runtime override would be invisible in precisely the record that exists to make it visible.


Five traps this package is built around

BigInt('') is 0n. So is BigInt(' '). An unreadable amount silently becoming a zero outflow is a transaction that spends and reports that it did not. Every amount is shape-checked with a regular expression before BigInt is called, and a rejected string returns null, never a default.

Number loses precision above 2^53. A u64 reaches 18_446_744_073_709_551_615; Number.MAX_SAFE_INTEGER is two thousand times smaller. Every amount here is a bigint.

The same coin type has several spellings. A live mainnet simulation reported 0x0000…0002::sui::SUI where every human and every config file writes 0x2::sui::SUI. A policy that stores one and compares it with the other matches nothing — a ceiling that never applies and an allow-list that never admits. Both sides go through normaliseType, always. Module and type names stay case-sensitive, because Move's are.

An empty list and an absent one are different facts. balanceChanges: [] means the node was asked and reported none. It does not mean the node was not asked. balanceChangesObserved is a required, separate boolean and balance-evidence refuses when it is false, because one of those states is "this moves no money" and the other is "we do not know what this moves".

Absence means opposite things on the two sides, and both readings are the strict one. An absent objectInputs is evidence nobody gathered, so object-input refuses it outright. An absent allowedObjects is authority nobody granted, so the same rule reads it as the empty list and permits no object at all. canonicalPolicyJson still encodes the two apart — null for a document written before objects were bounded, [] for one that bounds none — because they are different policies and an audit trail must not show them as the same one.


The ceiling is cumulative, and the window rolls

A per-transaction limit stops one large payment and does nothing about a thousand small ones. An agent driven by someone else's text is not limited to one attempt, and the cheapest attack on a per-transaction cap is a loop.

So ceilings are cumulative over a window of [nowMs - periodMs, nowMs], inclusive at both ends — excluding the boundary would open a one-millisecond hole a loop could be timed against. The window rolls; a calendar period resetting at midnight has a moment an attacker can wait for and then spend two full allowances across.

On SUI, gas counts. The node reports gas as an ordinary outflow: a one-MIST self-transfer measured live produced a balance change of -1088000, essentially all of it gas. Set the SUI ceiling high enough to cover it, and bound gas separately with maxGasBudgetMist.


The operator's bar: what may go out unattended

A ceiling answers "may this ever happen?". approvalThresholds answers a different question — "may this happen without the operator seeing it?" — and it is the only refusal in this package whose answer can be a person's yes instead of an edit to the policy.

approvalThresholds: [{ coinType: '0x2::sui::SUI', maxWithoutApproval: '500000' }]
// under the ceiling and under the bar  -> allow
// under the ceiling and over the bar   -> deny, approvalRequired: true
// over the ceiling                     -> deny, and no approval lifts it

An approval arrives on LedgerState.approvals, beside prior spend, because it is the same kind of fact: something the caller records and hands in. It carries a coin type, the total it covers and an expiry, and the rule checks all three.

The bar borrows the ceiling's window. There is no periodMs on a threshold and there will not be one. Two windows are two chances to get a window wrong, silently: a bar with a shorter window is a bar an agent walks under once per short window, which is a per-transaction limit in a rolling window's clothes and is defeated by the loop this package's ceilings exist to stop. Borrowing the ceiling's window also means an approval cannot be replayed — it is compared against the same cumulative total, so approving 5 SUI approves 5 SUI in that window, not one transaction of 5 SUI, repeated.

A bar at or above its own ceiling is refused by name. Every total large enough to cross it is already refused by the ceiling, so the operator would never be asked about anything — a gate that cannot fire, in a document whose author would read it as one that does. A false belief about a safety control, held by the one person the control exists for, is worse than an outage, because an outage gets noticed.

What this package cannot check is that the operator granted the approval. Verifying a signature means a cryptographic dependency and there are none here. An approval is a fact the caller asserts, exactly as a ledger entry is; @projectx-social/signer is where the operator's signature is verified. The two honest limitations are the same shape and neither is hidden.

approvalRequired is not a third outcome

It is a flag on a refusal. allow is still false, and every caller that reads allow and nothing else stops — including every caller written before the flag existed. What it says is narrow: this particular refusal is one an approval would have lifted, so a surface can say "your operator has to approve this" without matching on the wording of a reason. There is still no allowWithWarning, no override and no force.


The ledger is an input, and the honest limitation

LedgerState is passed in. This package persists nothing — where spend is stored is a decision with consequences a library cannot see. The ceiling is therefore only as good as the caller's record-keeping: sign without recording the spend, and the next evaluation sees a smaller total.

The chain-level bound described in @projectx-social/signer's README — funding the input coin at the ceiling so creator::take_price physically cannot take more — does not depend on this ledger and does not fail with it. That is the whole reason there are two bounds, and why they must stay independent.


Every rule fails closed

A rule that cannot decide — a malformed address, an unparseable amount, a name that does not normalise, a command kind that did not exist when this was written — refuses. None of them return "pass" on input they did not understand.

That is the property that makes an unrecognised future command kind, a renamed client-library field, or a corrupted policy file a refusal rather than a signature.