@keychains/client-sdk
v0.0.9
Published
Client SDK for Keychains.dev — make authenticated API calls through the Keychains proxy
Maintainers
Readme
@keychains/client-sdk
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.
Quickstart
1. Install
npm install @keychains/client-sdk2. Run with a fresh token
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.js3. Write your script
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.
Running multiple scripts
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.jsTemplate 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',
);
// Multi-account: target a specific account by identifier
await keychainsFetch('https://api.github.com/user', {
headers: { Authorization: 'Bearer {{OAUTH2_ACCESS_TOKEN}}' },
account: '[email protected]',
});What Happens Next
When you call keychainsFetch:
- URL rewriting —
https://api.lifx.com/v1/lights/allbecomeshttps://keychains.dev/api.lifx.com/v1/lights/all - Token injection — your permission token is sent via
X-Proxy-Authorizationso the proxy knows who you are - Scope check — the proxy verifies the user has approved the required credentials for this API
- Credential resolution — the proxy replaces
{{LIFX_PERSONAL_ACCESS_TOKEN}}with the real API key stored in 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 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.) |
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
Other SDKs
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.
Bug Reports & Feedback
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 [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!
