New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

@keychains/machine-sdk

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@keychains/machine-sdk

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

latest
npmnpm
Version
0.0.6
Version published
Maintainers
1
Created
Source

@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:

  • Registers the machine if no credentials exist in ~/.keychains/
  • Creates a wildcard permission (or reuses an existing one)
  • 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: 'work@company.com',
  },
});

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:

PrefixTypeSupported Variables
OAUTH2_OAuth 2.0 token{{OAUTH2_ACCESS_TOKEN}}, {{OAUTH2_REFRESH_TOKEN}}
OAUTH1_OAuth 1.0 token{{OAUTH1_ACCESS_TOKEN}}, {{OAUTH1_REQUEST_TOKEN}}
Anything elseAPI 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:

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

VariableDescription
KEYCHAINS_PERMISSION_IDPermission 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 you@example.com

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

More Info

Let's meet on keychains.dev!

Keywords

keychains

FAQs

Package last updated on 13 Mar 2026

Related posts