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

@agentvalet/client

v0.2.0

Published

AgentValet client SDK — call approved SaaS platforms from any Node agent. Signs the agent JWT, brokers through the AgentValet proxy, and waits out owner approvals.

Readme

@agentvalet/client

Call approved SaaS platforms from any Node agent. Your agent never holds the downstream credential — AgentValet signs a short-lived identity assertion, checks the call against the owner's grants and policy, injects the credential at call time, and writes an audit record.

Not in an MCP host? This is your package. If you are inside Claude Code, Claude Desktop, Cursor, or another MCP-aware host, install @agentvalet/mcp-server instead and call use_platform — you get the same guarantees with no code.

Building your own MCP server and want to enforce policy inside it? That's @agentvalet/mcp-broker.

Install

npm install @agentvalet/client

Node 18+. One dependency (jose, for RS256 signing).

Get an agent identity

npx @agentvalet/register

This generates an RSA keypair locally — the private key never leaves your machine — registers the public half, and writes ~/.agentvalet/agent.key.

Use it

import { AgentValet } from "@agentvalet/client";

const av = AgentValet.fromEnv();

const result = await av.call({
  platform: "slack",
  endpoint: "/api/chat.postMessage",
  method: "POST",
  scope: "chat:write",
  data: { channel: "#general", text: "Deploy finished." },
});

call() resolves to the broker's envelope — the upstream SaaS body under data, with _meta describing the call itself:

{
  data: { ok: true, ts: "1723800000.000100" },   // exactly what Slack returned
  _meta: { capability: "agent.action", timestamp: "…", next_actions: […], docs: "…" }
}

So it's result.data.ok, not result.ok. The split is deliberate: data is the upstream payload byte-for-byte, so anything the broker adds about the call stays outside it.

fromEnv() reads AGENTVALET_AGENT_ID / AGENTVALET_OWNER_ID (or the bare AGENT_ID / OWNER_ID the CLI writes) and finds the key via AGENT_PRIVATE_KEY_B64, AGENT_PRIVATE_KEY_PATH, AGENT_PRIVATE_KEY, or ~/.agentvalet/agent.key. To wire it up explicitly:

const av = new AgentValet({
  agentId: process.env.AGENT_ID!,
  ownerId: process.env.OWNER_ID!,
  privateKey: process.env.AGENT_PRIVATE_KEY!,
  proxyUrl: "https://api.agentvalet.ai",   // default
});

Approvals are just a slower call

When the owner has marked a scope as requiring approval, the proxy holds the action and call() waits. If the owner approves, the proxy re-runs the call and you get the result back — from your code's point of view it simply took longer.

const av = AgentValet.fromEnv({
  onApprovalPending: ({ elapsedMs }) =>
    console.log(`waiting for owner… ${Math.round(elapsedMs / 1000)}s`),
});

If nobody responds inside the budget (50s by default), you get an ApprovalTimeoutErrornot a failure. The action stays queued. Keep the approvalId and resume whenever you like:

import { ApprovalTimeoutError } from "@agentvalet/client";

try {
  await av.call({ platform: "stripe", endpoint: "/v1/refunds", method: "POST", scope: "charge" });
} catch (err) {
  if (err instanceof ApprovalTimeoutError) {
    await saveForLater(err.approvalId);          // e.g. into your job queue
  }
}

// …later, in another process:
const result = await av.waitForApproval(approvalId);

Set approvalTimeoutMs: 0 if you never want to block — call() then throws ApprovalTimeoutError the moment approval is required.

Errors you can branch on

Every throw is typed, so you never string-match an error envelope:

| Error | Means | What to do | |---|---|---| | ConfigError | Bad/missing identity or key | Fix config; thrown before any network call | | NetworkError | Transport failed | .message diagnoses DNS / TLS / firewall / timeout | | AccessDeniedError | No grant, or policy blocked it | requestAccess() — see below | | ApprovalDeniedError | Owner said no | Terminal. Don't retry | | ApprovalExpiredError | Aged out server-side | Re-issue the call | | ApprovalTimeoutError | You stopped waiting | Resume via waitForApproval(approvalId) | | UpstreamError | The SaaS itself returned non-2xx | .status / .data hold the upstream reply | | ProxyError | Anything else from the broker | .status / .body |

Asking for access you don't have

Deny-by-default means a scope you were never granted returns AccessDeniedError. Your agent can ask for it:

const { status } = await av.requestAccess({
  platform: "slack",
  scope: "chat:write",
  reason: "Post deploy notifications to #general",
});
if (status === "approved") { /* retry the original call */ }

Checking before you act

await av.listPlatforms();                  // what this agent is actually granted
await av.pendingActions();                 // queued behind an approval
await av.evaluate("stripe", "charge");     // dry-run the decision, no side effect

evaluate() is worth a call before anything destructive — it tells you whether the action would be allowed without performing it.

Which connection served the call

When a platform has several connections and you don't pin one, the broker uses the default. That's the quiet failure behind a lot of confused agents: you ask for a repo, the default GitHub account doesn't have it, you get a 404, and you conclude the repo doesn't exist rather than that you're on the wrong account.

When there was more than one connection you could have used, the response says so. On success it rides _meta, leaving data untouched:

res._meta.connection;
// {
//   used: "o-github", label: "Acme", defaulted: true,
//   others: [{ connection_id: "o-github-9f1c", label: "Personal" }],
//   hint: "…the target may live on another connection — retry with connection_id…"
// }

On an upstream error there's no envelope — the body is exactly what the platform sent — so the same information arrives as one namespaced key, carried on ProxyError.body:

{
  "message": "Not Found",
  "_agentvalet": { "connection": { "used": "o-github", "defaulted": true, "others": [], "hint": "…" } }
}

The upstream keys stay where they were, so existing error handling is unaffected. It's absent when there's nothing to act on — a connection you pinned, a platform with one connection, or a lookup that failed — so treat its absence as "no alternatives", never as "this is definitely the right account".

One identity per subagent

If your orchestrator fans work out to subagents, don't hand them your key. Mint each one a child identity: a bearer token that is scope-attenuated, time-boxed, independently revocable, and audited under its own agent id.

const child = await av.issueChild(
  [{ platform: "github", scopes: ["github:issues.read"] }],
  { name: "triage-worker", ttlSeconds: 600 },
);

// Hand the subagent the token and nothing else.
const sub = child.client();          // or AgentValet.fromBearer(child.bearerToken)
await sub.call({ platform: "github", endpoint: "/issues", scope: "github:issues.read" });

The proxy attenuates every grant against what you currently hold — you cannot delegate more than you have — and refuses the whole call if the intersection is empty. child.granted is what it actually issued, which is not necessarily what you asked for. Revoking or suspending the parent contains every child on its next call.

Bounds that are enforced server-side, and that you should design around rather than discover:

| | | |---|---| | Delegation depth | 1 — a child cannot mint children. issueChild() on a bearer client throws ConfigError locally. | | TTL | 60–3600s, default 900. No refresh endpoint — a longer run must re-mint from the parent. | | Live children | 25 per parent. | | Grants per call | 25. |

Tagging a run

sessionId tags everything a client does, so an audit reader can group one run — or tell a parent's calls from its children's.

const av = AgentValet.fromEnv({ sessionId: `run:${runId}` });

It rides the signed assertion as a session_id claim, or X-AV-Session in bearer mode, and also reads from AGENTVALET_SESSION_ID. Attribution only — the proxy never branches an authorization decision on it, and it is not verified to belong to the calling agent. Don't treat it as a security boundary.

Self-hosting

Point proxyUrl at your own deployment. Everything else is identical.

License

MIT