@openkeyai/sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@openkeyai/sdk`
2
2
 
3
- The SDK every [OpenKey AI](https://openkeyai.com) tool installs. Provides JWT verification, the `SecureKey` key-fetch pattern, and typed errors mirroring the hub's frozen error contract.
3
+ The SDK every [OpenKey AI](https://openkeyai.com) 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.
4
4
 
5
5
  ```bash
6
6
  pnpm add @openkeyai/sdk
@@ -9,18 +9,66 @@ pnpm add @openkeyai/sdk
9
9
 
10
10
  ## Status
11
11
 
12
- Five-module surface defined in [`hub/docs/TOOL_SDK.md`](https://github.com/Scott-Builds-AI/hub/blob/main/docs/TOOL_SDK.md). Status by module in 0.1.0:
12
+ Surface defined in [`hub/docs/TOOL_SDK.md`](https://github.com/OpenKeyAI/hub/blob/main/docs/TOOL_SDK.md). Status by module in 0.2.0:
13
13
 
14
- | Module | 0.1.0 | Notes |
14
+ | Module | 0.2.0 | Notes |
15
15
  |---|---|---|
16
16
  | `session.verify` | ✅ | Uses the hub's JWKS endpoint |
17
- | `keys.get` `SecureKey` | ✅ | Talks to `/api/tools/{slug}/keys/{provider}` |
17
+ | `proxy.call / .callRaw / .callStream` | ✅ | Phase 19 zero-trust proxy — plaintext key never enters your Worker |
18
+ | `openai.{images,chat,embeddings,audio}` | ✅ | Typed convenience over the proxy |
19
+ | `anthropic.messages` | ✅ | Typed convenience over the proxy |
20
+ | `replicate.predictions` | ✅ | Typed convenience over the proxy |
21
+ | `keys.get` → `SecureKey` | ✅ | Kept for endpoints the proxy doesn't typeset yet |
18
22
  | `user.profile` | ⏳ deferred | Needs hub to ship `/api/me` |
19
23
  | `billing.status` | ⏳ deferred | Needs hub to ship `/api/billing/status` |
20
24
  | `webhooks.handler` | ⏳ deferred | Lands with hub Phase 16 |
21
25
 
22
26
  Deferred modules are simply absent from exports — your tool gets a TypeScript error if it tries to use them, not a runtime surprise.
23
27
 
28
+ ## Which pattern should I use?
29
+
30
+ | You want to… | Use |
31
+ |---|---|
32
+ | Call a canonical model endpoint (OpenAI images, chat, Anthropic messages, Replicate predictions) | **Typed convenience** — `openai.images.generate(token, params)` etc. |
33
+ | Call any registered provider on any path / method we haven't typed yet | **Generic proxy** — `proxy.call(token, { provider, method, path, body })` |
34
+ | Get a binary response (TTS audio, image bytes) | `proxy.callRaw(...)` or `openai.audio.speech.create(...)` |
35
+ | Stream a response (chat SSE, chunked downloads) | `proxy.callStream(...)` or `openai.chat.completions.stream(...)` |
36
+ | Call a provider the hub doesn't route through yet, or run fine-tuning / batch / custom auth | `keys.get(token, provider)` + `SecureKey.use()` (legacy) |
37
+
38
+ **Default to the proxy.** The plaintext key never enters your tool process — every other consideration is downstream of that.
39
+
40
+ ## Quick start — proxy pattern (recommended, Phase 19+)
41
+
42
+ ```ts
43
+ import { session, openai, ProviderError, RateLimitedError, SubscriptionInactiveError } from "@openkeyai/sdk";
44
+
45
+ export async function handleRequest(req: Request) {
46
+ const jwt = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "") ?? "";
47
+
48
+ const claims = await session.verify(jwt, {
49
+ hubUrl: "https://openkeyai.com",
50
+ expectedAudience: "tool-youtube-thumbnail-generator",
51
+ });
52
+
53
+ try {
54
+ const result = await openai.images.generate(jwt, {
55
+ model: "gpt-image-1",
56
+ prompt: "Vibrant YouTube thumbnail for a tech tutorial",
57
+ n: 4,
58
+ size: "1792x1024",
59
+ });
60
+ return Response.json({ images: result.data });
61
+ } catch (err) {
62
+ if (err instanceof SubscriptionInactiveError) return new Response("Paywall", { status: 402 });
63
+ if (err instanceof RateLimitedError) return new Response("Slow down", { status: 429, headers: { "retry-after": String(err.retryAfter) } });
64
+ if (err instanceof ProviderError) return Response.json({ openai_error: err.body }, { status: err.upstreamStatus });
65
+ throw err;
66
+ }
67
+ }
68
+ ```
69
+
70
+ The plaintext OpenAI key is fetched + injected + scrubbed entirely inside the hub's Worker. Your tool process never sees `sk-proj-…`.
71
+
24
72
  ## Quick start
25
73
 
26
74
  A typical tool's request handler:
package/dist/index.cjs CHANGED
@@ -119,6 +119,41 @@ var SecureKeyConsumedError = class extends HubSdkError {
119
119
  this.name = "SecureKeyConsumedError";
120
120
  }
121
121
  };
122
+ var ProviderNotConfiguredError = class extends HubSdkError {
123
+ constructor(provider) {
124
+ super(
125
+ "provider_not_configured",
126
+ `Provider "${provider}" is not registered with the hub proxy. Available providers are listed at /api/proxy (coming) or in the hub's src/lib/proxy/providers.ts.`,
127
+ 404
128
+ );
129
+ this.name = "ProviderNotConfiguredError";
130
+ }
131
+ };
132
+ var ProviderError = class extends HubSdkError {
133
+ /** The provider slug we tried to call. */
134
+ provider;
135
+ /** The path on the provider that returned non-2xx. */
136
+ path;
137
+ /**
138
+ * The upstream response body. Parsed as JSON when the upstream's
139
+ * content-type was JSON; raw text otherwise.
140
+ */
141
+ body;
142
+ /** The upstream response status code (relayed unchanged). */
143
+ upstreamStatus;
144
+ constructor(provider, path, upstreamStatus, body) {
145
+ super(
146
+ "provider_error",
147
+ `${provider} ${path} returned ${upstreamStatus}.`,
148
+ upstreamStatus
149
+ );
150
+ this.name = "ProviderError";
151
+ this.provider = provider;
152
+ this.path = path;
153
+ this.upstreamStatus = upstreamStatus;
154
+ this.body = body;
155
+ }
156
+ };
122
157
  function errorFromResponse(status, body, context) {
123
158
  const code = body?.error ?? "internal";
124
159
  switch (code) {
@@ -136,6 +171,8 @@ function errorFromResponse(status, body, context) {
136
171
  return new NotSubscribedError();
137
172
  case "provider_not_granted":
138
173
  return new ProviderNotGrantedError(context.provider ?? "unknown");
174
+ case "provider_not_configured":
175
+ return new ProviderNotConfiguredError(context.provider ?? "unknown");
139
176
  case "rate_limited":
140
177
  return new RateLimitedError(context.retryAfter ?? 60);
141
178
  case "key_not_found":
@@ -145,6 +182,25 @@ function errorFromResponse(status, body, context) {
145
182
  return new InternalError(status);
146
183
  }
147
184
  }
185
+ function isHubErrorBody(body) {
186
+ if (typeof body !== "object" || body === null) return false;
187
+ const errorField = body.error;
188
+ if (typeof errorField !== "string") return false;
189
+ const knownCodes = [
190
+ "missing_token",
191
+ "bad_token",
192
+ "missing_scope",
193
+ "subscription_inactive",
194
+ "tool_not_found",
195
+ "not_subscribed",
196
+ "provider_not_granted",
197
+ "rate_limited",
198
+ "key_not_found",
199
+ "internal",
200
+ "provider_not_configured"
201
+ ];
202
+ return knownCodes.includes(errorField);
203
+ }
148
204
  var resolvers = /* @__PURE__ */ new Map();
149
205
  function getJwksResolver(hubUrl) {
150
206
  const cached = resolvers.get(hubUrl);
@@ -199,6 +255,7 @@ async function verify(jwt, opts = {}) {
199
255
  const jti = payload.jti;
200
256
  const iat = payload.iat;
201
257
  const exp = payload.exp;
258
+ const rawMode = payload.mode;
202
259
  if (typeof sub !== "string" || sub.length === 0) {
203
260
  throw new BadTokenError("sub claim missing or invalid.");
204
261
  }
@@ -217,12 +274,19 @@ async function verify(jwt, opts = {}) {
217
274
  if (typeof iat !== "number" || typeof exp !== "number") {
218
275
  throw new BadTokenError("iat / exp claims missing or invalid.");
219
276
  }
277
+ const mode = typeof rawMode === "string" && (rawMode === "production" || rawMode === "owner_test") ? rawMode : rawMode === void 0 ? "production" : null;
278
+ if (mode === null) {
279
+ throw new BadTokenError(
280
+ `mode claim must be 'production' or 'owner_test' (got ${JSON.stringify(rawMode)}).`
281
+ );
282
+ }
220
283
  return {
221
284
  iss: "https://openkeyai.com",
222
285
  sub,
223
286
  aud,
224
287
  scopes,
225
288
  subscription_active: subscriptionActive,
289
+ mode,
226
290
  iat,
227
291
  exp,
228
292
  jti
@@ -357,6 +421,298 @@ async function get(jwt, provider, opts = {}) {
357
421
  });
358
422
  }
359
423
 
424
+ // src/proxy.ts
425
+ var PROXY_PATH = "/api/proxy";
426
+ function ensureBearer(token) {
427
+ if (!token || typeof token !== "string") {
428
+ throw new BadTokenError("No JWT provided.");
429
+ }
430
+ }
431
+ function buildUrl(opts) {
432
+ if (!opts.provider) {
433
+ throw new BadTokenError("ProxyCallOptions.provider is required.");
434
+ }
435
+ if (!opts.path || !opts.path.startsWith("/")) {
436
+ throw new BadTokenError(
437
+ "ProxyCallOptions.path must start with '/' (e.g. '/v1/chat/completions')."
438
+ );
439
+ }
440
+ const hubUrl = opts.hubUrl ?? "https://openkeyai.com";
441
+ return new URL(
442
+ `${PROXY_PATH}/${encodeURIComponent(opts.provider)}${opts.path}`,
443
+ hubUrl
444
+ );
445
+ }
446
+ function buildRequestInit(token, opts) {
447
+ const headers = new Headers();
448
+ if (opts.headers) {
449
+ for (const [k, v] of Object.entries(opts.headers)) {
450
+ headers.set(k, v);
451
+ }
452
+ }
453
+ headers.set("authorization", `Bearer ${token}`);
454
+ let body;
455
+ if (opts.method === "GET" || opts.method === "DELETE" || opts.body == null) {
456
+ body = void 0;
457
+ } else if (typeof opts.body === "string" || opts.body instanceof Blob || opts.body instanceof FormData || opts.body instanceof ArrayBuffer || opts.body instanceof ReadableStream) {
458
+ body = opts.body;
459
+ } else {
460
+ body = JSON.stringify(opts.body);
461
+ if (!headers.has("content-type")) {
462
+ headers.set("content-type", "application/json");
463
+ }
464
+ }
465
+ if (!headers.has("accept")) {
466
+ headers.set("accept", "application/json");
467
+ }
468
+ return {
469
+ method: opts.method,
470
+ headers,
471
+ body,
472
+ signal: opts.signal,
473
+ // @ts-expect-error duplex is part of fetch's RequestInit on Workers + recent Node
474
+ duplex: body instanceof ReadableStream ? "half" : void 0
475
+ };
476
+ }
477
+ async function rawFetch(token, opts) {
478
+ ensureBearer(token);
479
+ const url = buildUrl(opts);
480
+ try {
481
+ return await fetch(url.toString(), buildRequestInit(token, opts));
482
+ } catch (err) {
483
+ if (err instanceof DOMException && err.name === "AbortError") {
484
+ throw err;
485
+ }
486
+ throw new NetworkError(err);
487
+ }
488
+ }
489
+ async function parseBodyByContentType(response) {
490
+ const ct = response.headers.get("content-type") ?? "";
491
+ if (ct.includes("application/json")) {
492
+ try {
493
+ return await response.json();
494
+ } catch {
495
+ return null;
496
+ }
497
+ }
498
+ try {
499
+ return await response.text();
500
+ } catch {
501
+ return null;
502
+ }
503
+ }
504
+ async function handleResponse(response, opts, expectJson) {
505
+ if (response.ok) {
506
+ if (!expectJson) return response;
507
+ return await response.json();
508
+ }
509
+ const body = await parseBodyByContentType(response);
510
+ if (isHubErrorBody(body)) {
511
+ const retryAfter = parseInt(response.headers.get("retry-after") ?? "60", 10);
512
+ throw errorFromResponse(
513
+ response.status,
514
+ { error: body.error },
515
+ {
516
+ provider: opts.provider,
517
+ scopeNeeded: "keys.read",
518
+ retryAfter: Number.isFinite(retryAfter) ? retryAfter : 60
519
+ }
520
+ );
521
+ }
522
+ throw new ProviderError(opts.provider, opts.path, response.status, body);
523
+ }
524
+ async function call(token, opts) {
525
+ const response = await rawFetch(token, opts);
526
+ return handleResponse(response, opts, true);
527
+ }
528
+ async function callRaw(token, opts) {
529
+ const response = await rawFetch(token, opts);
530
+ return handleResponse(response, opts, false);
531
+ }
532
+ async function callStream(token, opts) {
533
+ const response = await rawFetch(token, opts);
534
+ if (!response.ok) {
535
+ const body = await parseBodyByContentType(response);
536
+ if (isHubErrorBody(body)) {
537
+ const retryAfter = parseInt(
538
+ response.headers.get("retry-after") ?? "60",
539
+ 10
540
+ );
541
+ throw errorFromResponse(
542
+ response.status,
543
+ { error: body.error },
544
+ {
545
+ provider: opts.provider,
546
+ scopeNeeded: "keys.read",
547
+ retryAfter: Number.isFinite(retryAfter) ? retryAfter : 60
548
+ }
549
+ );
550
+ }
551
+ throw new ProviderError(opts.provider, opts.path, response.status, body);
552
+ }
553
+ if (response.body == null) {
554
+ throw new BadTokenError("Upstream returned 2xx with no body to stream.");
555
+ }
556
+ return response.body;
557
+ }
558
+
559
+ // src/providers/openai.ts
560
+ var openai = {
561
+ images: {
562
+ generate(token, params, opts = {}) {
563
+ return call(token, {
564
+ ...opts,
565
+ provider: "openai",
566
+ method: "POST",
567
+ path: "/v1/images/generations",
568
+ body: params
569
+ });
570
+ }
571
+ },
572
+ chat: {
573
+ completions: {
574
+ create(token, params, opts = {}) {
575
+ return call(token, {
576
+ ...opts,
577
+ provider: "openai",
578
+ method: "POST",
579
+ path: "/v1/chat/completions",
580
+ body: params
581
+ });
582
+ },
583
+ stream(token, params, opts = {}) {
584
+ return callStream(token, {
585
+ ...opts,
586
+ provider: "openai",
587
+ method: "POST",
588
+ path: "/v1/chat/completions",
589
+ body: { ...params, stream: true }
590
+ });
591
+ }
592
+ }
593
+ },
594
+ embeddings: {
595
+ create(token, params, opts = {}) {
596
+ return call(token, {
597
+ ...opts,
598
+ provider: "openai",
599
+ method: "POST",
600
+ path: "/v1/embeddings",
601
+ body: params
602
+ });
603
+ }
604
+ },
605
+ audio: {
606
+ transcriptions: {
607
+ create(token, params, opts = {}) {
608
+ const fd = new FormData();
609
+ fd.append("file", params.file, params.filename ?? "audio.webm");
610
+ fd.append("model", params.model);
611
+ if (params.language) fd.append("language", params.language);
612
+ if (params.prompt) fd.append("prompt", params.prompt);
613
+ if (params.response_format)
614
+ fd.append("response_format", params.response_format);
615
+ if (params.temperature !== void 0)
616
+ fd.append("temperature", String(params.temperature));
617
+ return call(token, {
618
+ ...opts,
619
+ provider: "openai",
620
+ method: "POST",
621
+ path: "/v1/audio/transcriptions",
622
+ body: fd
623
+ });
624
+ }
625
+ },
626
+ speech: {
627
+ create(token, params, opts = {}) {
628
+ return callRaw(token, {
629
+ ...opts,
630
+ provider: "openai",
631
+ method: "POST",
632
+ path: "/v1/audio/speech",
633
+ body: params
634
+ });
635
+ }
636
+ }
637
+ }
638
+ };
639
+
640
+ // src/providers/anthropic.ts
641
+ var anthropic = {
642
+ messages: {
643
+ create(token, params, opts = {}) {
644
+ return call(token, {
645
+ ...opts,
646
+ provider: "anthropic",
647
+ method: "POST",
648
+ path: "/v1/messages",
649
+ body: params
650
+ });
651
+ },
652
+ stream(token, params, opts = {}) {
653
+ return callStream(token, {
654
+ ...opts,
655
+ provider: "anthropic",
656
+ method: "POST",
657
+ path: "/v1/messages",
658
+ body: { ...params, stream: true }
659
+ });
660
+ }
661
+ }
662
+ };
663
+
664
+ // src/providers/replicate.ts
665
+ var replicate = {
666
+ predictions: {
667
+ create(token, params, opts = {}) {
668
+ return call(token, {
669
+ ...opts,
670
+ provider: "replicate",
671
+ method: "POST",
672
+ path: "/v1/predictions",
673
+ body: params
674
+ });
675
+ },
676
+ get(token, predictionId, opts = {}) {
677
+ return call(token, {
678
+ ...opts,
679
+ provider: "replicate",
680
+ method: "GET",
681
+ path: `/v1/predictions/${encodeURIComponent(predictionId)}`
682
+ });
683
+ },
684
+ cancel(token, predictionId, opts = {}) {
685
+ return call(token, {
686
+ ...opts,
687
+ provider: "replicate",
688
+ method: "POST",
689
+ path: `/v1/predictions/${encodeURIComponent(predictionId)}/cancel`
690
+ });
691
+ },
692
+ /**
693
+ * Convenience: create + poll until the prediction reaches a terminal
694
+ * status. Polls every `pollIntervalMs` (default 1000ms) with the
695
+ * provided AbortSignal honoured.
696
+ *
697
+ * For long-running predictions consider using webhooks via
698
+ * `create({ webhook, webhook_events_filter })` instead so you're not
699
+ * holding open a long fetch.
700
+ */
701
+ async run(token, params, opts = {}) {
702
+ const interval = opts.pollIntervalMs ?? 1e3;
703
+ let prediction = await this.create(token, params, opts);
704
+ while (prediction.status === "starting" || prediction.status === "processing") {
705
+ if (opts.signal?.aborted) {
706
+ throw opts.signal.reason ?? new DOMException("Aborted", "AbortError");
707
+ }
708
+ await new Promise((resolve) => setTimeout(resolve, interval));
709
+ prediction = await this.get(token, prediction.id, opts);
710
+ }
711
+ return prediction;
712
+ }
713
+ }
714
+ };
715
+
360
716
  // src/index.ts
361
717
  var session = {
362
718
  verify
@@ -364,7 +720,12 @@ var session = {
364
720
  var keys = {
365
721
  get
366
722
  };
367
- var SDK_VERSION = "0.1.0";
723
+ var proxy = {
724
+ call,
725
+ callRaw,
726
+ callStream
727
+ };
728
+ var SDK_VERSION = "0.2.0";
368
729
 
369
730
  exports.BadTokenError = BadTokenError;
370
731
  exports.HubSdkError = HubSdkError;
@@ -374,6 +735,8 @@ exports.MissingScopeError = MissingScopeError;
374
735
  exports.MissingTokenError = MissingTokenError;
375
736
  exports.NetworkError = NetworkError;
376
737
  exports.NotSubscribedError = NotSubscribedError;
738
+ exports.ProviderError = ProviderError;
739
+ exports.ProviderNotConfiguredError = ProviderNotConfiguredError;
377
740
  exports.ProviderNotGrantedError = ProviderNotGrantedError;
378
741
  exports.RateLimitedError = RateLimitedError;
379
742
  exports.SDK_VERSION = SDK_VERSION;
@@ -381,7 +744,11 @@ exports.SecureKey = SecureKey;
381
744
  exports.SecureKeyConsumedError = SecureKeyConsumedError;
382
745
  exports.SubscriptionInactiveError = SubscriptionInactiveError;
383
746
  exports.ToolNotFoundError = ToolNotFoundError;
747
+ exports.anthropic = anthropic;
384
748
  exports.keys = keys;
749
+ exports.openai = openai;
750
+ exports.proxy = proxy;
751
+ exports.replicate = replicate;
385
752
  exports.session = session;
386
753
  //# sourceMappingURL=index.cjs.map
387
754
  //# sourceMappingURL=index.cjs.map