
Company News
Socket Joins New OpenJS Program to Fund Node.js Security Work
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.
@keychains/machine-sdk
Advanced tools
Machine SDK for Keychains.dev — register machines, create permissions, mint tokens, and make proxy calls
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-sdkinstead. It only requires a pre-minted permission token and performs proxied fetch — no file system needed.
npm install @keychains/machine-sdk
keychainsFetch as a drop-in replacement for fetchThe 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:
~/.keychains/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 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. |
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',
);
When you call keychainsFetch:
~/.keychains/credentials.json doesn't exist, generates an Ed25519 keypair and registers with the Keychains serverscopes is provided)https://api.lifx.com/v1/lights/all becomes https://keychains.dev/api.lifx.com/v1/lights/all{{LIFX_PERSONAL_ACCESS_TOKEN}} with the real API key from the user's vaultWith 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
}
}
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.
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');
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();
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);
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,
);
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.
| Variable | Description |
|---|---|
KEYCHAINS_PERMISSION_ID | Permission ID for auto-resolution (used by keychainsFetch) |
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.
Let's meet on keychains.dev!
FAQs
Machine SDK for Keychains.dev — register machines, create permissions, mint tokens, and make proxy calls
We found that @keychains/machine-sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Company News
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.

Research
/Security News
A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover.