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

@keychains/machine-sdk

v0.0.6

Published

Machine SDK for Keychains.dev — register machines, create permissions, mint tokens, and make proxy calls

Readme

@keychains/machine-sdk

Full-lifecycle SDK for making authenticated API calls through the Keychains.dev proxy from machines you control — servers, dev laptops, CI runners, or any environment with persistent file storage.

The Machine SDK handles everything automatically: machine registration, permission management, token minting, and proxied API calls. Credentials are stored in ~/.keychains/ and reused across runs.

Running in a serverless or stateless environment? Use the lighter @keychains/client-sdk instead. It only requires a pre-minted permission token and performs proxied fetch — no file system needed.

Quickstart

1. Install

npm install @keychains/machine-sdk

2. Use keychainsFetch as a drop-in replacement for fetch

The only difference? You can replace any credential with a template variable.

import { keychainsFetch } from '@keychains/machine-sdk';

// Gmail — get last 10 emails from my inbox
const res = await keychainsFetch(
  'https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=10',
  {
    headers: {
      Authorization: 'Bearer {{OAUTH2_ACCESS_TOKEN}}',
    },
  },
);

const emails = await res.json();
console.log(emails);

On the first call, the SDK automatically:

  1. Registers the machine if no credentials exist in ~/.keychains/
  2. Creates a wildcard permission (or reuses an existing one)
  3. Mints a permission token, caches it, and makes the proxied request

You can customize the auto-setup via the keychains property (stripped before passing to fetch):

// Custom machine name and scoped permission
const res = await keychainsFetch('https://api.lifx.com/v1/lights/all', {
  headers: { Authorization: 'Bearer {{LIFX_PERSONAL_ACCESS_TOKEN}}' },
  keychains: {
    machineName: 'my-home-server',
    permissionName: 'LIFX Controller',
    scopes: ['lifx::key::LIFX_PERSONAL_ACCESS_TOKEN'],
  },
});

// Multi-account: target a specific account by identifier
const res2 = await keychainsFetch('https://api.github.com/user', {
  headers: { Authorization: 'Bearer {{OAUTH2_ACCESS_TOKEN}}' },
  keychains: {
    account: '[email protected]',
  },
});

Template Variables

How to write them

Template variables use the {{VARIABLE_NAME}} syntax. The variable name tells the proxy which type of credential to inject:

| Prefix | Type | Supported Variables | |--------|------|---------------------| | OAUTH2_ | OAuth 2.0 token | {{OAUTH2_ACCESS_TOKEN}}, {{OAUTH2_REFRESH_TOKEN}} | | OAUTH1_ | OAuth 1.0 token | {{OAUTH1_ACCESS_TOKEN}}, {{OAUTH1_REQUEST_TOKEN}} | | Anything else | API key | {{LIFX_PERSONAL_ACCESS_TOKEN}}, {{OPENAI_API_KEY}}, etc. |

Where to put them

Place them exactly where you'd normally put the real credential — headers, body, or query parameters:

// In a header (most common)
await keychainsFetch('https://api.lifx.com/v1/lights/all', {
  headers: { Authorization: 'Bearer {{LIFX_PERSONAL_ACCESS_TOKEN}}' },
});

// In the request body
await keychainsFetch('https://slack.com/api/chat.postMessage', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer {{OAUTH2_ACCESS_TOKEN}}',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ channel: '#general', text: 'Hello!' }),
});

// In query parameters
await keychainsFetch(
  'https://api.example.com/data?api_key={{MY_API_KEY}}&format=json',
);

What Happens Next

When you call keychainsFetch:

  1. Machine registration — if ~/.keychains/credentials.json doesn't exist, generates an Ed25519 keypair and registers with the Keychains server
  2. Permission resolution — finds an existing active permission or creates a new one (wildcard by default, scoped if scopes is provided)
  3. Token minting — authenticates via SSH challenge-response and mints a short-lived permission token (cached and auto-refreshed)
  4. URL rewritinghttps://api.lifx.com/v1/lights/all becomes https://keychains.dev/api.lifx.com/v1/lights/all
  5. Credential resolution — the proxy replaces {{LIFX_PERSONAL_ACCESS_TOKEN}} with the real API key from the user's vault
  6. Request forwarding — the proxy forwards the request to the upstream API with real credentials injected
  7. Response passthrough — the upstream response is returned to you as-is

Handling missing approvals

With wildcard permissions, users approve scopes on demand. The first time your code hits a new API, the user may not have approved it yet. When that happens, keychainsFetch throws a KeychainsError with an approvalUrl — share it with the user so they can grant access:

import { keychainsFetch, KeychainsError } from '@keychains/machine-sdk';

try {
  const res = await keychainsFetch('https://api.github.com/user', {
    headers: { Authorization: 'Bearer {{OAUTH2_ACCESS_TOKEN}}' },
  });
  console.log(await res.json());
} catch (err) {
  if (err instanceof KeychainsError && err.approvalUrl) {
    // The user hasn't approved GitHub yet — show them the link
    console.log('Please approve access:', err.approvalUrl);
    // Once approved, retry the same call and it will succeed
  }
}

Security benefits

  • Secrets never leave the Keychains.dev servers — your code, logs, and environment stay clean
  • Users approve exactly which scopes and APIs an agent can access
  • Credentials can only be sent to the APIs of the providers they belong to
  • Every proxied request is audited with full traceability
  • Permissions can be revoked instantly from the dashboard

User Identity

getUser(config?)

Get the identity of the user linked to this machine.

import { getUser } from '@keychains/machine-sdk';

const user = await getUser();
console.log(user.userId, user.email, user.name);

Returns { userId, email?, name?, telegramUsername? }. Throws if the machine is not yet linked to a user account.


Connection Management

List and revoke the user's connected providers (OAuth connections, API keys, etc.):

listConnections(opts?, config?)

List all connections for the linked user. Optionally filter by provider or status.

import { listConnections } from '@keychains/machine-sdk';

// All active connections
const connections = await listConnections();

// Filter by provider
const githubConns = await listConnections({ provider: 'github' });

// Include all statuses
const all = await listConnections({ status: 'all' });

Each connection includes id, provider, type (oauth or api_key), status, displayName, scopes, accountIdentifier, and timestamps.

revokeConnection(connectionId, config?)

Revoke (delete) a connection. The server cleans up credentials asynchronously.

import { revokeConnection } from '@keychains/machine-sdk';

await revokeConnection('conn_abc123');

Machine Management

These functions handle machine identity and SSH key lifecycle:

register(name, config?)

Register a new machine. Generates an Ed25519 keypair, stores it in ~/.keychains/, and registers the public key with the server.

import { register } from '@keychains/machine-sdk';

const creds = await register('my-build-server');
console.log('Machine ID:', creds.machineId);

authenticate(config?)

Authenticate via SSH challenge-response. Returns a short-lived machine JWT.

import { authenticate } from '@keychains/machine-sdk';

const { token, userId, expiresAt } = await authenticate();

rotateKeys(config?)

Rotate the SSH keypair — generates new keys and re-registers with the server. Old keys are backed up.

import { rotateKeys } from '@keychains/machine-sdk';

const newCreds = await rotateKeys();

Permission Management

Permissions control which APIs and scopes a machine can access:

createPermissionRequest(opts, config?)

Create a new permission. Wildcard permissions let scopes be approved on demand; scoped permissions require pre-defined scopes.

import { createPermissionRequest } from '@keychains/machine-sdk';

// Wildcard (scopes approved dynamically)
const { permission, approvalUrl } = await createPermissionRequest({
  name: 'my-agent',
  wildcard: true,
  allowDelegation: true,
});

// Scoped (pre-defined scopes)
const { permission: scoped } = await createPermissionRequest({
  name: 'github-reader',
  requestedScopes: ['github::repo', 'github::read:user'],
});

listPermissions(config?)

List all permissions for this machine.

import { listPermissions } from '@keychains/machine-sdk';

const permissions = await listPermissions();

mintPermissionToken(requestId, ttlSeconds?, config?)

Mint a short-lived permission token for use with the proxy or the Client SDK.

import { mintPermissionToken } from '@keychains/machine-sdk';

const { token, expiresIn } = await mintPermissionToken(permission.id, 600);

checkScopes(requestId, scopes, config?)

Check if specific scopes are approved for a permission.

import { checkScopes } from '@keychains/machine-sdk';

const { allowed, missing, approvalUrl } = await checkScopes(
  permission.id,
  ['github::repo'],
);

revokePermission(requestId, config?)

Revoke a permission permanently.

import { revokePermission } from '@keychains/machine-sdk';

await revokePermission(permission.id);

Delegation

Delegates allow a machine to grant sub-permissions to remote systems (e.g., a CI runner, a cloud function):

createDelegate(permissionRequestId, opts?, config?)

Create a delegate identity with its own Ed25519 keypair. Ship the private key to the remote system.

import { createDelegate } from '@keychains/machine-sdk';

const { delegate, privateKey } = await createDelegate(permission.id, {
  mode: 'wildcard',
  allowDelegation: false,
});

// Ship `privateKey` and `delegate.delegateId` to the remote system

mintDelegateToken(delegateId, privateKey, ttlSeconds?, config?)

Mint a permission token using a delegate's private key (runs on the remote system).

import { mintDelegateToken } from '@keychains/machine-sdk';

const { token, expiresIn } = await mintDelegateToken(
  delegateId,
  privateKey,
  300,
);

Lower-level Proxy

If you manage machine registration and permissions yourself, use proxyFetch directly:

import { proxyFetch } from '@keychains/machine-sdk';

const res = await proxyFetch('https://api.github.com/user/repos', {
  permissionRequestId: 'pr_abc123',
  method: 'GET',
  headers: { Authorization: 'Bearer {{OAUTH2_ACCESS_TOKEN}}' },
});

// Multi-account: target a specific account
const res2 = await proxyFetch('https://api.github.com/user', {
  permissionRequestId: 'pr_abc123',
  account: '[email protected]',
  headers: { Authorization: 'Bearer {{OAUTH2_ACCESS_TOKEN}}' },
});

proxyFetch handles token minting, caching, auto-refresh on 401, and throws KeychainsError on 403. The optional account parameter targets a specific account when the user has multiple accounts connected for the same provider.


Environment Variables

| Variable | Description | |----------|-------------| | KEYCHAINS_PERMISSION_ID | Permission ID for auto-resolution (used by keychainsFetch) |


Bug Reports & Feedback

Found a bug or have a suggestion? Submit it straight from your terminal:

# Report a bug
keychains feedback "The proxy returns 502 on large POST bodies"

# Send feedback
keychains feedback --type feedback "Love the wildcard permissions!"

# With more detail
keychains feedback --type bug --title "502 on large POST" --description "When sending >1MB body to Slack API..." --contact [email protected]

The keychains feedback command (alias: keychains bug) sends your report directly to the engineering team.


More Info

Let's meet on keychains.dev!