@keychains/server-sdk
v0.0.2
Published
Server SDK for Keychains.dev (BETA) — register trusted apps, create permissions, mint tokens, and make proxy calls via JWKS authentication
Maintainers
Readme
@keychains/server-sdk
BETA — This SDK is under active development. APIs may change.
Server SDK for Keychains.dev — give your server-side app secure access to user credentials (OAuth tokens, API keys) through the Keychains proxy.
Setup
npm install @keychains/server-sdk
npx @keychains/server-sdk register myapp.comThe register script will:
- Generate an RSA keypair and write
jwks.jsonto the right location for your project - Walk you through DNS verification
- Add
KEYCHAINS_*env vars to your.env
Quickstart
import { KeychainsApp } from '@keychains/server-sdk';
const keychains = KeychainsApp.fromEnv();Option 1: Approval at request time (recommended)
Everything is transparent until the proxy request — identical to how keychains curl works. A wildcard permission is created automatically, and the auth URL only surfaces when the user actually needs to connect a service.
// One-liner — fetch through the proxy:
try {
const res = await keychains.forUser(userId).withTask('Project Albatros').fetch('https://api.github.com/user');
const data = await res.json();
} catch (err) {
if (err instanceof KeychainsAppError && err.approvalUrl) {
// User hasn't connected GitHub yet — show them this URL
console.log(`Please authorize: ${err.approvalUrl}`);
}
}Or step by step:
// Get a token (creates a wildcard permission if needed)
const { token } = await keychains.forUser(userId).withTask('Project Albatros').getToken();
// Use the token for one or more proxy requests
const res = await keychains.fetch('https://api.github.com/user', { permissionToken: token });When to use: Quick prototyping, AI agents, cases where you don't know upfront which services the user will need.
Option 2: Pre-approving requests
Create a scoped permission with specific scopes. The user approves upfront, then all subsequent requests go through without interruption.
const scopes = ['gmail.com::oauth2::read', 'linear.app::oauth2::read'];
const permission = await keychains.forUser(userId).withTask('Project Albatros').getPermission(scopes);
// Show approval URL to the user
console.log('Please approve at: ' + permission.approvalUrl());
// Check if approved (poll or after redirect)
if (await permission.isApproved()) {
const res = await permission.fetch('https://gmail.googleapis.com/gmail/v1/users/me/messages');
// or: const { token } = await permission.getToken();
}When to use: Apps with explicit user consent UIs, when you know exactly which services are needed.
API Reference
KeychainsApp
// From environment variables (recommended)
const keychains = KeychainsApp.fromEnv();
// Manual configuration
const keychains = new KeychainsApp({
domain: 'myapp.com',
privateKey: process.env.KEYCHAINS_PRIVATE_KEY!,
keyId: 'key-1',
serverUrl: 'https://keychains.dev', // optional
});
keychains.setAppId(process.env.KEYCHAINS_APP_ID!);Fluent API
| Method | Returns | Description |
|--------|---------|-------------|
| .forUser(userId) | UserContext | Scope operations to a user |
| .forUser(id).withTask(name) | TaskContext | Scope to a user + task |
| .forUser(id).withTask(name).fetch(url, opts?) | Response | Wildcard token + proxied fetch |
| .forUser(id).withTask(name).getToken() | TokenResult | Wildcard token for manual fetch |
| .forUser(id).withTask(name).getPermission(scopes?) | Permission | Scoped permission for pre-approval |
| .forUser(id).getTokenForTask(name) | TokenResult | Shortcut for .withTask(name).getToken() |
| .forUser(id).getPermissionForTask(name, scopes) | Permission | Shortcut for .withTask(name).getPermission(scopes) |
Lower-level methods
| Method | Description |
|--------|-------------|
| .createPermission(opts) | Create a permission request |
| .listPermissions(appUserId) | List permissions for a user |
| .getPermissionStatus(id, appUserId) | Check permission status |
| .mintToken(permissionId, appUserId, ttl?) | Mint a short-lived token |
| .fetch(url, { permissionToken }) | Proxied fetch with a token |
| .register({ verify? }) | DNS registration flow |
| .delegate(permissionId, appUserId, opts) | Delegate access to VMs |
Permission
| Method | Returns | Description |
|--------|---------|-------------|
| .approvalUrl() | string \| undefined | URL for user to approve |
| .isApproved() | boolean | Poll if permission is active |
| .getToken(ttl?) | TokenResult | Mint a token |
| .fetch(url, opts?) | Response | Mint token + proxied fetch |
Error Handling
import { KeychainsAppError } from '@keychains/server-sdk';
try {
const res = await keychains.forUser(userId).withTask('My Task').fetch(url);
} catch (err) {
if (err instanceof KeychainsAppError) {
if (err.approvalUrl) {
// User needs to authorize — redirect or show the URL
console.log(`Authorize: ${err.approvalUrl}`);
} else {
console.error(`${err.code}: ${err.message}`);
}
}
}| Code | Description |
|------|-------------|
| authorization_required | Proxy request needs user approval (has approvalUrl) |
| missing_env | Required environment variables not set |
| not_registered | App ID not set — run register or call setAppId() |
| unauthorized | Invalid or expired JWT |
| not_found | Permission or app not found |
| forbidden | Permission revoked or app revoked |
Registration Flow
1. Run the register script
npx @keychains/server-sdk register myapp.comThis generates a keypair, writes jwks.json, and walks you through DNS verification.
2. Serve the JWKS endpoint
Your domain must serve the JWKS file at:
https://myapp.com/.well-known/keychains.dev/jwks.jsonFor Next.js / static sites, the register script places it in public/ by default.
3. DNS verification
Add a TXT record as instructed by the script. Once verified, your app is registered and the env vars are written to .env.
Delegation
Delegate access to VMs or sub-agents:
const delegate = await keychains.delegate(permissionId, userId, {
publicKey: vmPublicKey,
scopes: ['github.com::oauth2::repo'],
});
// Or use bootstrap token for self-registration
const delegate = await keychains.delegate(permissionId, userId, {
useBootstrapToken: true,
scopes: ['github.com::oauth2::repo'],
});
// delegate.bootstrapToken → send to VMSecurity Model
- JWKS Authentication: Your app signs JWTs with a private key; Keychains verifies via your published JWKS
- Domain Verification: DNS TXT record proves domain ownership
- User Sovereignty: Users authenticate on keychains.dev and explicitly authorize access. Compromising your signing key does NOT grant access to user credentials.
- Key Rotation: Add new keys to JWKS before removing old ones. 24-hour grace period on key disappearance.
Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| KEYCHAINS_DOMAIN | Yes | Your registered domain |
| KEYCHAINS_PRIVATE_KEY | Yes | PEM-encoded private key |
| KEYCHAINS_KEY_ID | Yes | Key ID (kid) from your JWKS |
| KEYCHAINS_APP_ID | No | App ID (auto-set by register script) |
| KEYCHAINS_SERVER_URL | No | Override server URL (default: https://keychains.dev) |
License
Proprietary — Interagentic Inc.
