@openkeyai/sdk
v0.4.1
Published
The SDK every OpenKey AI tool installs. Zero-trust proxy + SecureKey key-fetch + JWT verify.
Downloads
755
Readme
@openkeyai/sdk
The SDK every OpenKey AI tool installs. JWT verification, the zero-trust API proxy, typed convenience methods for OpenAI / Anthropic / Replicate, and typed errors mirroring the hub's frozen error contract.
pnpm add @openkeyai/sdk
# or: npm i @openkeyai/sdkStatus
Surface defined in hub/docs/TOOL_SDK.md. Status by module in 0.2.0:
| Module | 0.2.0 | Notes |
|---|---|---|
| session.verify | ✅ | Uses the hub's JWKS endpoint |
| proxy.call / .callRaw / .callStream | ✅ | Phase 19 zero-trust proxy — plaintext key never enters your Worker |
| openai.{images,chat,embeddings,audio} | ✅ | Typed convenience over the proxy |
| anthropic.messages | ✅ | Typed convenience over the proxy |
| replicate.predictions | ✅ | Typed convenience over the proxy |
| keys.get → SecureKey | ✅ | Kept for endpoints the proxy doesn't typeset yet |
| user.profile | ⏳ deferred | Needs hub to ship /api/me |
| billing.status | ⏳ deferred | Needs hub to ship /api/billing/status |
| webhooks.handler | ⏳ deferred | Lands with hub Phase 16 |
Deferred modules are simply absent from exports — your tool gets a TypeScript error if it tries to use them, not a runtime surprise.
Which pattern should I use?
| You want to… | Use |
|---|---|
| Call a canonical model endpoint (OpenAI images, chat, Anthropic messages, Replicate predictions) | Typed convenience — openai.images.generate(token, params) etc. |
| Call any registered provider on any path / method we haven't typed yet | Generic proxy — proxy.call(token, { provider, method, path, body }) |
| Get a binary response (TTS audio, image bytes) | proxy.callRaw(...) or openai.audio.speech.create(...) |
| Stream a response (chat SSE, chunked downloads) | proxy.callStream(...) or openai.chat.completions.stream(...) |
| Call a provider the hub doesn't route through yet, or run fine-tuning / batch / custom auth | keys.get(token, provider) + SecureKey.use() (legacy) |
Default to the proxy. The plaintext key never enters your tool process — every other consideration is downstream of that.
Quick start — proxy pattern (recommended, Phase 19+)
import { session, openai, ProviderError, RateLimitedError, SubscriptionInactiveError } from "@openkeyai/sdk";
export async function handleRequest(req: Request) {
const jwt = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "") ?? "";
const claims = await session.verify(jwt, {
hubUrl: "https://openkeyai.com",
expectedAudience: "tool-youtube-thumbnail-generator",
});
try {
const result = await openai.images.generate(jwt, {
model: "gpt-image-1",
prompt: "Vibrant YouTube thumbnail for a tech tutorial",
n: 4,
size: "1792x1024",
});
return Response.json({ images: result.data });
} catch (err) {
if (err instanceof SubscriptionInactiveError) return new Response("Paywall", { status: 402 });
if (err instanceof RateLimitedError) return new Response("Slow down", { status: 429, headers: { "retry-after": String(err.retryAfter) } });
if (err instanceof ProviderError) return Response.json({ openai_error: err.body }, { status: err.upstreamStatus });
throw err;
}
}The plaintext OpenAI key is fetched + injected + scrubbed entirely inside the hub's Worker. Your tool process never sees sk-proj-….
Quick start
A typical tool's request handler:
import { session, keys, SubscriptionInactiveError, RateLimitedError } from "@openkeyai/sdk";
export async function handleRequest(req: Request) {
const jwt = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "") ?? "";
// 1. Verify the JWT and read claims.
const claims = await session.verify(jwt, {
expectedAudience: "yt-thumbnails", // your tool's slug
});
// 2. Fetch the user's API key for the provider you need.
try {
const k = await keys.get(jwt, "openai");
// 3. Use it inside a single callback. After this, the reference is scrubbed.
const response = await k.use(async (apiKey) => {
return fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: {
"authorization": `Bearer ${apiKey}`,
"content-type": "application/json",
},
body: JSON.stringify({ model: "gpt-4o", input: "Hello!" }),
});
});
return response;
} catch (e) {
if (e instanceof SubscriptionInactiveError) {
return new Response("Upgrade your subscription", { status: 402 });
}
if (e instanceof RateLimitedError) {
return new Response(`Too many calls. Retry in ${e.retryAfter}s.`, {
status: 429,
headers: { "retry-after": String(e.retryAfter) },
});
}
throw e;
}
}SecureKey — the never-leak pattern
const k = await keys.get(jwt, "anthropic"); // SecureKey
await k.use(async (apiKey) => {
// `apiKey` is the plaintext string. Available ONLY inside this callback.
return fetch("https://api.anthropic.com/...", {
headers: { "x-api-key": apiKey },
});
});
// After the callback resolves (or throws):
// - The internal reference is scrubbed
// - Subsequent `k.use(...)` throws SecureKeyConsumedError
// - Each instance is single-use; call keys.get() again for the next request
// Leakage guards (verified by the test suite):
k.toString() // → "[SecureKey]"
JSON.stringify(k) // → '"[SecureKey]"'
`${k}` // → "[SecureKey]"
console.log(k) // → [SecureKey]The plaintext is held in a true private field (#plaintext) — not visible via Object.keys, bracket notation, or any serialiser.
Typed errors
The hub's frozen error contract (defined in hub/docs/phases/05-tools-keyfetch.md) maps 1:1 onto SDK error classes:
| Code | Class | When |
|---|---|---|
| missing_token | MissingTokenError | No Authorization header |
| bad_token | BadTokenError | Sig / iss / aud / exp / shape failed |
| missing_scope | MissingScopeError | Token lacks the scope this endpoint requires |
| subscription_inactive | SubscriptionInactiveError | User's subscription not active — show paywall, don't retry |
| tool_not_found | ToolNotFoundError | Slug unknown or suspended |
| not_subscribed | NotSubscribedError | User hasn't enabled the tool yet |
| provider_not_granted | ProviderNotGrantedError | Tool / subscription doesn't include the provider |
| rate_limited | RateLimitedError | .retryAfter carries the seconds value |
| key_not_found | KeyNotFoundError | User has no key for this provider |
| internal | InternalError | Hub-side error — retry-with-backoff is fine |
| network | NetworkError | Could not reach the hub at all |
| secure_key_consumed | SecureKeyConsumedError | .use() called more than once on the same SecureKey |
Every class extends HubSdkError so you can catch the base type if you don't care about the specific code.
Configuration
The SDK has no configuration object and no environment variables. Pass overrides per call:
await session.verify(jwt, { hubUrl: "https://staging.openkeyai.com" });
await keys.get(jwt, "openai", { hubUrl: "https://staging.openkeyai.com", signal });Defaults are https://openkeyai.com everywhere.
Versioning
- Semver. Breaking changes to public surface = major bump with 60 days notice (per
hub/CLAUDE.md). SDK_VERSIONis exported so tools can log which build they shipped against.- Don't import from
@openkeyai/sdk/_internal/*— the tool-manifest scanner (Phase 9) treats it as a CI failure.
Development
pnpm install
pnpm dev # tsup --watch
pnpm build # tsup
pnpm typecheck
pnpm test # vitest (focused tests on SecureKey leakage guarantees)License
MIT — see LICENSE.
