learn-secrets-sdk
v1.11.18
Published
Secrets management SDK for static sites with Learn platform
Maintainers
Readme
learn-secrets-sdk
secure api proxy for static sites. call external apis (openai, anthropic, stripe, etc.) without exposing api keys.
zero-config: no tokens or credentials in your code. authentication via origin header.
quick start (cli)
1. install cli
npm install -g learn-secrets-sdk2. authenticate
learn-secrets login3. initialize project
learn-secrets init --project YOUR_PROJECT_ID --origins yourdomain.com,localhost --env .envauto-detects api keys from .env and uploads them. configures origins for your site.
get project id: visit ctklearn.carsontkempf.workers.dev → copy from sidebar
4. install sdk in project
npm install learn-secrets-sdk5. use in code
import { SecretsSDK } from 'learn-secrets-sdk';
// zero-config - no credentials needed
const sdk = new SecretsSDK();
// call openai securely
const response = await sdk.post('openai', '/v1/chat/completions', {
model: 'gpt-4',
messages: [{ role: 'user', content: 'hello' }]
});how it works:
- browser sends
Originheader automatically - proxy validates origin matches configured domains
- api key injected server-side
- no tokens or credentials in client code
features
- zero-config - no tokens, appid, or credentials in code
- origin-based auth - automatic browser security via origin header
- api keys secured - stored server-side, never exposed to clients
- auto-detection - cli detects openai, anthropic, stripe, github, google
- works anywhere - cloudflare pages, netlify, vercel, any static host
- rate limiting - 100 req/min per domain built-in
- typescript - full type definitions included
cli commands
login
learn-secrets loginauthenticate via browser oauth. credentials saved to ~/.learn/credentials.json.
init
learn-secrets init \
--project abc123xyz \ # your project id
--origins example.com,localhost \ # allowed domains
--env .env # path to .env filereads .env, detects api keys, uploads to project, configures origins.
auto-detects:
OPENAI_API_KEY→ openai providerANTHROPIC_API_KEY→ anthropic providerSTRIPE_SECRET_KEY→ stripe providerGITHUB_TOKEN→ github provider*_API_KEY→ custom provider
full cli docs: cli reference
api methods
const sdk = new SecretsSDK();
// http methods
await sdk.get(keyName, endpoint, headers?)
await sdk.post(keyName, endpoint, body, headers?)
await sdk.put(keyName, endpoint, body, headers?)
await sdk.delete(keyName, endpoint, headers?)
await sdk.patch(keyName, endpoint, body, headers?)
// custom request
await sdk.call(keyName, endpoint, {
method: 'POST',
body: { },
headers: { }
})constructor options
zero-config (recommended):
const sdk = new SecretsSDK();uses origin header for authentication. no credentials needed.
advanced options:
const sdk = new SecretsSDK({
baseUrl?: string, // custom proxy url (default: production)
timeout?: number, // request timeout ms (default: 30000)
retryOn429?: boolean // auto-retry on rate limit (default: true)
});error handling
import { SecretsSDK, SecretsSDKError, OriginMismatchError, RateLimitError } from 'learn-secrets-sdk';
const sdk = new SecretsSDK();
try {
const response = await sdk.post('openai', '/v1/chat/completions', data);
} catch (error) {
if (error instanceof OriginMismatchError) {
// domain not configured in allowed origins
console.error('add domain to origins in dashboard');
} else if (error instanceof RateLimitError) {
// exceeded 100 req/min
console.log('retry after:', error.retryAfter, 'seconds');
} else if (error instanceof SecretsSDKError) {
// other api errors
console.log('status:', error.status);
console.log('message:', error.message);
}
}usage tracking
const sdk = new SecretsSDK();
await sdk.post('openai', '/v1/chat/completions', data);
// check rate limit status
const usage = sdk.getUsage();
console.log('remaining:', usage.remaining); // requests left this minute
console.log('limit:', usage.limit); // 100
console.log('resets at:', usage.reset); // unix timestampexamples
vanilla javascript
<!DOCTYPE html>
<html>
<body>
<button onclick="callAPI()">call ai</button>
<div id="response"></div>
<script type="module">
import { SecretsSDK } from './node_modules/learn-secrets-sdk/dist/index.js';
// zero-config
const sdk = new SecretsSDK();
window.callAPI = async function() {
const result = await sdk.post('openai', '/v1/chat/completions', {
model: 'gpt-4',
messages: [{ role: 'user', content: 'hello' }]
});
document.getElementById('response').textContent =
result.choices[0].message.content;
};
</script>
</body>
</html>react
import { useMemo } from 'react';
import { SecretsSDK } from 'learn-secrets-sdk';
function App() {
// zero-config sdk
const sdk = useMemo(() => new SecretsSDK(), []);
const handleClick = async () => {
const result = await sdk.post('openai', '/v1/chat/completions', {
model: 'gpt-4',
messages: [{ role: 'user', content: 'hello' }]
});
console.log(result.choices[0].message.content);
};
return <button onClick={handleClick}>call ai</button>;
}vue
import { ref } from 'vue';
import { SecretsSDK } from 'learn-secrets-sdk';
// zero-config sdk
const sdk = new SecretsSDK();
const response = ref('');
async function callAPI() {
const result = await sdk.post('openai', '/v1/chat/completions', {
model: 'gpt-4',
messages: [{ role: 'user', content: 'hello' }]
});
response.value = result.choices[0].message.content;
}next.js
'use client';
import { useCallback, useMemo } from 'react';
import { SecretsSDK } from 'learn-secrets-sdk';
export default function ChatPage() {
// zero-config sdk
const sdk = useMemo(() => new SecretsSDK(), []);
const chat = useCallback(async (message: string) => {
const result = await sdk.post('openai', '/v1/chat/completions', {
model: 'gpt-4',
messages: [{ role: 'user', content: message }]
});
return result.choices[0].message.content;
}, [sdk]);
// ... rest of component
}supported providers
auto-configured base urls when using cli:
- openai (
OPENAI_API_KEY) - anthropic (
ANTHROPIC_API_KEY) - stripe (
STRIPE_SECRET_KEY) - github (
GITHUB_TOKEN) - google (
GOOGLE_API_KEY)
custom providers supported - configure base url in dashboard.
how it works
1. cli uploads api keys to proxy server
2. cli configures allowed origins (yourdomain.com, localhost, etc.)
3. your static site imports sdk: new SecretsSDK()
4. browser automatically sends Origin header
5. proxy validates origin matches configured list
6. proxy injects api key server-side
7. external api called with key
8. response returned to browsersecurity:
- api keys never sent to browser
- origin header enforced by browser (cannot be spoofed)
- stolen code useless from unauthorized domains
security
security model
the sdk uses origin-based authentication specifically designed for static sites. here's how it protects your api keys:
zero credentials in code:
- no api keys, tokens, or appid in your javascript
- anyone can view your source code - nothing sensitive to steal
- works because authentication happens via browser origin header
browser origin header:
- automatically sent by browser on every request
- contains the domain making the request (e.g.,
https://yourdomain.com) - cannot be spoofed - browsers prevent javascript from modifying it
- enforced by browser security model (same-origin policy)
server-side validation:
1. browser sends request with Origin: https://yourdomain.com
2. proxy checks if yourdomain.com is in allowed origins list
3. if match: proxy injects api key server-side and calls external api
4. if no match: request rejected with 403 error
5. api keys never sent to browserwhat if someone steals your code?
- they deploy it on their domain (e.g.,
https://attacker.com) - browser sends
Origin: https://attacker.com - proxy rejects - their domain not in your origins list
- api calls fail - they can't use your api keys
comparison to alternatives:
traditional approach (insecure):
// api key exposed in code
const apiKey = 'sk-proj-abc123...';
fetch('https://api.openai.com/v1/chat/completions', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});problem: anyone viewing source code gets your api key
environment variables (doesn't work for static sites):
// process.env doesn't exist in browser
const apiKey = process.env.OPENAI_API_KEY; // undefinedproblem: static sites have no server to hold env vars
backend proxy (requires server):
// requires deploying and maintaining your own backend
fetch('https://yourbackend.com/api/openai', { ... });problem: need to deploy and maintain server infrastructure
learn-secrets approach (secure for static sites):
// zero credentials - authentication via origin header
const sdk = new SecretsSDK();
await sdk.post('openai', '/v1/chat/completions', { ... });solution: browser origin header provides authentication
security guarantees
- api keys never exposed - stored server-side, injected in proxy layer
- origin header enforced - browsers prevent spoofing, validated server-side
- no credentials to leak - zero-config means no tokens in your code
- rate limiting - 100 req/min per domain prevents abuse
- instant revocation - remove origins in dashboard to block access immediately
- encrypted storage - api keys encrypted at rest in database
- https only - sdk enforces https in production
best practices
allowed origins:
- only add domains you control
- use specific domains instead of wildcards
- localhost allowed for development only
- remove test domains before going live
deployment:
- deploy to production domain first
- then add that domain to allowed origins
- test that api calls work from production
- never commit credentials to version control
monitoring:
- check usage dashboard regularly
- watch for unexpected rate limit hits
- review origins list periodically
- remove unused origins
incident response:
- if domain compromised: remove from origins list immediately
- if api key leaked: regenerate in provider dashboard
- if suspecting abuse: check usage logs in dashboard
documentation
troubleshooting
403 origin not allowed:
- run
learn-secrets initwith correct--origins - or update origins in dashboard: settings → sdk tokens
404 api key not found:
- verify key name matches .env variable (e.g.,
OPENAI_API_KEY→openai) - re-run
learn-secrets initto upload keys
429 rate limit:
- 100 requests per minute per domain
- implement throttling or caching in your app
license
mit
