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

@kinnet/mcp-identity

v0.4.1

Published

Library half of the MCP credential shim: probe an RFC 9728 resource document, bind its claimed kinnet participant id to a signed discovery record, select the token mode, and mint and cache a self-issued pnc1 chain token.

Downloads

289

Readme

@kinnet/mcp-identity

The library half of the MCP credential shim: what lets one kinnet identity walk into an MCP server as itself, instead of as a per-server OAuth account.

The kinnet CLI and the kinnet-mcp stdio shim are its only intended consumers; the shapes below are the contract they rely on.

Five functions, in the order a shim calls them.

1. probeResourceMetadata(resourceUrl, options?)

Fetches the server's RFC 9728 protected-resource document, trying the path-inserted location (https://host/.well-known/oauth-protected-resource/mcp for https://host/mcp) and then the root one. A kinnet-aware server adds two top-level members to the standard ones:

{
  "resource": "https://resource.example/mcp",
  "authorization_servers": ["https://id.kinnet.example"],
  "scopes_supported": ["notes/read", "notes/write", "notes/share", "notes/publish"],
  "bearer_methods_supported": ["header"],
  "kinnet_participant_id": "pk_z…", // the id a token's `aud` would be bound to — a CLAIM
  "kinnet_token_formats": ["pnc1"] // "pnc1" ⇒ the chain door is open
}

The document is parsed loosely (unknown members are kept, because RFC 9728 is an extensible registry) but fetched strictly: redirect: "error", a 64 KiB ceiling enforced while reading rather than after, a deadline, and strict JSON parsing. Failures are an McpIdentityError with a reason: insecure_url, unreachable, too_large, document_malformed, document_invalid.

A location that fails to answer (network error, refused redirect, 404, 5xx) is a reason to try the next one. A location that answers with an unusable document is a hard failure — the server has told us what it publishes, and a second lookup would only let a broken document hide.

https always; http only on a loopback host (localhost, 127.0.0.1, [::1]), the same rule the IdP applies to redirect URIs.

2. bindResourceParticipant({ metadata, resourceUrl, discovery }) — the binding rule

kinnet_participant_id is served unauthenticated at the server's own origin, so it is a claim, not evidence. Without a cross-check, a hostile server X publishes the resource R's participant id, the shim mints a chain token with aud = R, X takes it as a bearer and replays it at R as R's own caller.

So the claim is checked against a record that participant signed, read from the shim's OWN configured discovery and never from a URL the server supplied: ParticipantProfile.verifiedDomains (spec 017) — the participant's signed assertion that it operates a service at that host. X cannot make R's profile say x.example.

The matching rule, one verifiedDomains entry at a time (verifiedDomainMatches, exported and pure):

  • an entry is host or host:port, lowercase; an IPv6 literal is bracketed ([::1]:8787);
  • the URL's host must equal the entry's host, case-insensitively and exactly. No suffix matching, no prefix matching, no wildcards, no subdomain inference: resource.example matches neither evil-resource.example nor resource.example.attacker nor sub.resource.example, all of which an attacker can register;
  • an entry with a port must equal the URL's effective port (explicit, or the scheme's default, so resource.example:443 matches https://resource.example/mcp); an entry with no port matches any port.

The profile itself is verified before a word of it is trusted: the participant's key log is replayed under replayKeyLogStatesFor (bound to the id it was fetched for, so a hostile discovery host cannot serve one participant's log at another's path), and the profile's scalar signature is lifted into a one-member signature set and checked against any state the log ever committed (spec 015 S5, 017 READER) — a rotation must not orphan a profile published before it.

The verdict is data, not an exception:

{ ok: true, participantId } |
{ ok: false, reason: "no_participant_id" | "insecure_scheme" | "profile_unavailable"
                   | "profile_signature_invalid" | "host_not_asserted" }

discovery is anything with getKeyLog and getProfile — a DiscoveryClient from @kinnet/discovery-client satisfies it structurally.

3. selectMode({ metadata, binding, kinnetIssuer })

In order:

  1. kinnet_participant_id present and kinnet_token_formats includes "pnc1" and the binding holds ⇒ { mode: "A" } — self-issue. Needs no IdP to be reachable.
  2. else authorization_servers includes the configured kinnet issuer (exact string compare after trimming trailing slashes) ⇒ { mode: "C", issuer } — go through the IdP.
  3. else { mode: "none", reason: "not_kinnet_aware" } — the harness's own OAuth applies and the shim has no job.

A server that offers the chain door but fails the binding check is { mode: "none", reason: "binding_failed" } and is never downgraded to C. A downgrade would hide exactly the attack the binding check exists for behind a working OAuth login.

4. mintChainToken({ issuer, parent?, resourceParticipantId, abilities, ttlSeconds?, now? })

Mints the mode-A token: a leaf grant with audienceId = the resource's participant id (a participant audience, not a key) and caveats.aud naming that same id, encoded as pnc1. + base64url(JSON chain). Byte-compatible with what the IdP issues and what a resource verifies today.

Two lanes, one function, because the leaf is the same record in both and only its parentage differs:

  • Service lane (no parent) — the identity acts for itself, so the leaf is a self-issued root (issueGrant), subjectId = issuerId = the service, and chain is one link.
  • Machine lane (parent) — the identity is a machine participant acting for a human, so the leaf is delegated from the custody-signed human → machine grant (issueDelegatedGrant), issuerId = the machine, proof = canonicalDigest(parent), and subjectId stays the human: one chain carries one participant's authority however many hands it passes through (spec 009 rule 4), so the resource sees who is asking rather than merely which program asked. chain is [leaf, parent]leaf first, the order encodeChainAccessToken defines and every verifier reads.

Everything downstream — the encoding, the cache, the resource's verification — is unchanged by the extra link.

No ceilings are applied here. The resource enforces its own — on every link expiresAt present and every ability at least two / segments and never notes/admin, and on the leaf a remaining lifetime of at most 7 days — and a minter that silently clamped a caller's TTL would hand back a token whose lifetime nobody asked for. An over-long TTL produces a valid, signed, refused token, which is the honest outcome.

Attenuation is not checked here either: a DelegationError from @kinnet/trust propagates unwrapped. A leaf asking for an ability the parent never held, or for a verifier outside the parent's aud, is a caller bug that the trust package already names precisely — which rule, and which value offended. Re-throwing it as an McpIdentityError would replace a diagnosis with the word invalid_argument.

selectParentGrant(grants, { machineId, resourceParticipantId, abilities, now? })

Which held human → machine grant backs a token for a given server. A machine accumulates one grant per server the human consented to at kinnet mcp add, each signed in custody under a passkey ceremony; this answers "is there already consent for this server and these abilities?", which is what decides whether the next mcp add is silent or costs a browser tap.

Returns the first grant in input order satisfying all four rules, or null. First-match is deliberate: precedence between two eligible grants (newest, narrowest, last consented) is a policy the caller holds. Sorting is the caller's job; choosing is this function's.

  1. audienceId === machineId — authority is exercised only by the principal it was delegated to (spec 009 rule 3), and issueDelegatedGrant would refuse to sign under anything else.
  2. abilities literally contains every requested ability — exact string membership, not prefix cover. See below; this is the load-bearing rule.
  3. caveats.aud, if present, includes resourceParticipantId. aud narrows only along a chain (spec 011), so a leaf can never name a server its parent did not — a grant consented for prod is not a grant for staging. A malformed aud is a non-match, not a throw: a grant nobody here can read is a grant nobody here should spend, and one corrupt entry must not stop the rest of the pile from being searched.
  4. Unexpired. expiresAt may legally be absent on a participant-audience grant (spec 011 makes it mandatory only for a key audience), even though the device-grant flow always sets one at 90 days; absent means "does not expire", not "invalid".

Why ability matching is literal and not prefix-covered. Spec 009 rule 5 lets a verifier prefix-cover notes/read out of a parent holding notes — but the bearer door a kinnet-aware resource puts in front of a pnc1. chain applies its two-segment/no-admin rule to every link and matches the leaf's abilities by exact string against its allowlist, so an umbrella parent such as ["notes"] is refused there. Prefix cover is for verifiers deciding a chain; a bearer chain must show explicit consent at each link. A selector using abilityCovers would hand back a grant that mints a perfectly valid, signed token no resource accepts — and would silently defeat the per-server consent unit the whole design rests on.

This function only selects. Whether the resulting two-link chain is acceptable is the door's judgement, and the door port in this package's suite is what asserts it. Pure: no I/O, no signature checks (these are the machine's own grants, verified when they were stored), no throwing on malformed input.

createTokenCache({ mint, refreshFraction?, now? })

Holds one resource's token in memory only — a pnc1. chain is a bearer credential carrying the caller's full consented authority, and a copy on disk would be a second copy of that authority with a lifetime nobody manages.

  • get() serves the cached token until 80% of its lifetime has passed, then re-mints. Re-minting before expiry means a request never carries a token that dies in flight.
  • Concurrent get()s on a cold or stale cache share one mint (single-flight).
  • A failed mint rejects the callers waiting on it and leaves the cache untouched; the next get() tries again.
  • invalidate() drops the token — including against a mint that was already in the air, which resolves its own callers but no longer repopulates the cache.
  • peek() reports the cached token and its timings without minting.

Testing

pnpm --filter @kinnet/mcp-identity test. Discovery is not stubbed: the suite stands up a real @kinnet/discovery-api app over a memory store and publishes real signed records through @kinnet/discovery-client. The suite carries a port of the door a kinnet-aware resource puts in front of a pnc1. bearer — verifyGrantChain with verifierId + requireAud, then the chain[0].audienceId equality step, then the resource's ceilings — so minted tokens are asserted against what a resource actually enforces rather than against what the minter believes it produced.

Those ceilings are per-link, following the policy a chain-verifying resource applies: on every link of the chain expiresAt must be present and parseable and every ability must have at least two non-empty / segments and never be notes/admin. Only the remaining-lifetime cap (7 days) is leaf-only — an upstream link is a standing consent measured in months, and a resource's own 90-day upstream cap is its policy number, deliberately not modelled in the oracle.

License

Free to use, not open source (yet). The package is published under a use-only license: install it and build on it, commercially included, but the software may not be modified or redistributed — see LICENSE for the exact terms. The implementation ships minified with full TypeScript declarations. The intent is to open the source as the network matures.