
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
@keychains/client-sdk
Advanced tools
Client SDK for Keychains.dev — make authenticated API calls through the Keychains proxy
Minimal, zero-dependency SDK for making authenticated API calls through the Keychains.dev proxy. Your code never touches real credentials — the proxy injects them at runtime.
npm install @keychains/client-sdk
The keychains token command registers your machine (if needed), creates a wildcard permission, and mints a short-lived proxy token — all in one step:
KEYCHAINS_TOKEN=$(npx -y keychains token) \
node your_script.js
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/client-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);
That's it. The proxy resolves {{OAUTH2_ACCESS_TOKEN}} with the user's real Google OAuth token — your code never sees it.
Tokens expire after 15 minutes. To reuse the same token across multiple commands in a shell session, use eval:
eval $(npx -y keychains token --env)
# KEYCHAINS_TOKEN is now set for the next 15 minutes
node script_a.js
node script_b.js
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',
);
// Multi-account: target a specific account by identifier
await keychainsFetch('https://api.github.com/user', {
headers: { Authorization: 'Bearer {{OAUTH2_ACCESS_TOKEN}}' },
account: 'work@company.com',
});
When you call keychainsFetch:
https://api.lifx.com/v1/lights/all becomes https://keychains.dev/api.lifx.com/v1/lights/allX-Proxy-Authorization so the proxy knows who you are{{LIFX_PERSONAL_ACCESS_TOKEN}} with the real API key stored in 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 an InsufficientScopeError containing an approvalUrl — share it with the user so they can grant access:
import { keychainsFetch, InsufficientScopeError } from '@keychains/client-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 InsufficientScopeError) {
// 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
}
}
The error includes useful details:
| Property | Type | Description |
|---|---|---|
approvalUrl | string? | URL the user should visit to approve the missing scopes |
missingScopes | string[]? | Scopes that need approval |
refusedScopes | string[]? | Scopes explicitly refused by the user |
code | string | Error code (insufficient_scope, scope_refused, permission_denied, etc.) |
We also offer @keychains/machine-sdk for standalone machines and long-running agents. It handles machine registration, permission creation, token minting, and delegation — all from Node.js, without the CLI.
Found a bug or have a suggestion? Submit it straight from your terminal:
# Report a bug
npx -y keychains feedback "The proxy returns 502 on large POST bodies"
# Send feedback
npx -y keychains feedback --type feedback "Love the wildcard permissions!"
# With more detail
npx -y 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
Client SDK for Keychains.dev — make authenticated API calls through the Keychains proxy
We found that @keychains/client-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.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.