@openkeyai/sdk 0.1.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/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/index.cjs +387 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +267 -0
- package/dist/index.d.ts +267 -0
- package/dist/index.js +369 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { jwtVerify, errors, decodeJwt, createRemoteJWKSet } from 'jose';
|
|
2
|
+
|
|
3
|
+
// src/session.ts
|
|
4
|
+
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
var HubSdkError = class extends Error {
|
|
7
|
+
code;
|
|
8
|
+
/** HTTP status from the hub, when applicable. 0 for client-only errors. */
|
|
9
|
+
status;
|
|
10
|
+
constructor(code, message, status = 0) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "HubSdkError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.status = status;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var MissingTokenError = class extends HubSdkError {
|
|
18
|
+
constructor() {
|
|
19
|
+
super("missing_token", "No Authorization token was provided.", 401);
|
|
20
|
+
this.name = "MissingTokenError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var BadTokenError = class extends HubSdkError {
|
|
24
|
+
constructor(message = "Token signature / issuer / audience / expiry check failed.") {
|
|
25
|
+
super("bad_token", message, 401);
|
|
26
|
+
this.name = "BadTokenError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var MissingScopeError = class extends HubSdkError {
|
|
30
|
+
constructor(needed) {
|
|
31
|
+
super("missing_scope", `Token is missing the required scope: ${needed}.`, 403);
|
|
32
|
+
this.name = "MissingScopeError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var SubscriptionInactiveError = class extends HubSdkError {
|
|
36
|
+
constructor() {
|
|
37
|
+
super(
|
|
38
|
+
"subscription_inactive",
|
|
39
|
+
"The user's subscription is not active. Surface a paywall \u2014 don't retry.",
|
|
40
|
+
402
|
|
41
|
+
);
|
|
42
|
+
this.name = "SubscriptionInactiveError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var ToolNotFoundError = class extends HubSdkError {
|
|
46
|
+
constructor() {
|
|
47
|
+
super("tool_not_found", "Tool unknown or not approved.", 404);
|
|
48
|
+
this.name = "ToolNotFoundError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var NotSubscribedError = class extends HubSdkError {
|
|
52
|
+
constructor() {
|
|
53
|
+
super(
|
|
54
|
+
"not_subscribed",
|
|
55
|
+
"The user has not enabled this tool in their hub settings.",
|
|
56
|
+
403
|
|
57
|
+
);
|
|
58
|
+
this.name = "NotSubscribedError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var ProviderNotGrantedError = class extends HubSdkError {
|
|
62
|
+
constructor(provider) {
|
|
63
|
+
super(
|
|
64
|
+
"provider_not_granted",
|
|
65
|
+
`User has not granted this tool access to the ${provider} provider.`,
|
|
66
|
+
403
|
|
67
|
+
);
|
|
68
|
+
this.name = "ProviderNotGrantedError";
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var RateLimitedError = class extends HubSdkError {
|
|
72
|
+
retryAfter;
|
|
73
|
+
constructor(retryAfter) {
|
|
74
|
+
super(
|
|
75
|
+
"rate_limited",
|
|
76
|
+
`Rate limit hit. Retry after ${retryAfter} seconds.`,
|
|
77
|
+
429
|
|
78
|
+
);
|
|
79
|
+
this.name = "RateLimitedError";
|
|
80
|
+
this.retryAfter = retryAfter;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var KeyNotFoundError = class extends HubSdkError {
|
|
84
|
+
constructor(provider) {
|
|
85
|
+
super(
|
|
86
|
+
"key_not_found",
|
|
87
|
+
`No ${provider} key configured for this user. The tool should prompt the user to add one.`,
|
|
88
|
+
404
|
|
89
|
+
);
|
|
90
|
+
this.name = "KeyNotFoundError";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var InternalError = class extends HubSdkError {
|
|
94
|
+
constructor(status, message = "The hub returned an internal error.") {
|
|
95
|
+
super("internal", message, status);
|
|
96
|
+
this.name = "InternalError";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var NetworkError = class extends HubSdkError {
|
|
100
|
+
constructor(cause) {
|
|
101
|
+
super(
|
|
102
|
+
"network",
|
|
103
|
+
`Could not reach the hub: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
104
|
+
0
|
|
105
|
+
);
|
|
106
|
+
this.name = "NetworkError";
|
|
107
|
+
this.cause = cause;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var SecureKeyConsumedError = class extends HubSdkError {
|
|
111
|
+
constructor() {
|
|
112
|
+
super(
|
|
113
|
+
"secure_key_consumed",
|
|
114
|
+
"SecureKey has already been used. Each instance is single-use; call keys.get() again to fetch a fresh one.",
|
|
115
|
+
0
|
|
116
|
+
);
|
|
117
|
+
this.name = "SecureKeyConsumedError";
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
function errorFromResponse(status, body, context) {
|
|
121
|
+
const code = body?.error ?? "internal";
|
|
122
|
+
switch (code) {
|
|
123
|
+
case "missing_token":
|
|
124
|
+
return new MissingTokenError();
|
|
125
|
+
case "bad_token":
|
|
126
|
+
return new BadTokenError();
|
|
127
|
+
case "missing_scope":
|
|
128
|
+
return new MissingScopeError(context.scopeNeeded ?? "unknown");
|
|
129
|
+
case "subscription_inactive":
|
|
130
|
+
return new SubscriptionInactiveError();
|
|
131
|
+
case "tool_not_found":
|
|
132
|
+
return new ToolNotFoundError();
|
|
133
|
+
case "not_subscribed":
|
|
134
|
+
return new NotSubscribedError();
|
|
135
|
+
case "provider_not_granted":
|
|
136
|
+
return new ProviderNotGrantedError(context.provider ?? "unknown");
|
|
137
|
+
case "rate_limited":
|
|
138
|
+
return new RateLimitedError(context.retryAfter ?? 60);
|
|
139
|
+
case "key_not_found":
|
|
140
|
+
return new KeyNotFoundError(context.provider ?? "unknown");
|
|
141
|
+
case "internal":
|
|
142
|
+
default:
|
|
143
|
+
return new InternalError(status);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
var resolvers = /* @__PURE__ */ new Map();
|
|
147
|
+
function getJwksResolver(hubUrl) {
|
|
148
|
+
const cached = resolvers.get(hubUrl);
|
|
149
|
+
if (cached) return cached;
|
|
150
|
+
const url = new URL("/.well-known/jwks.json", hubUrl);
|
|
151
|
+
const resolver = createRemoteJWKSet(url, {
|
|
152
|
+
// Respect the hub's stated cache window (5 min strong + 10 min SWR).
|
|
153
|
+
cacheMaxAge: 5 * 60 * 1e3,
|
|
154
|
+
cooldownDuration: 30 * 1e3
|
|
155
|
+
});
|
|
156
|
+
resolvers.set(hubUrl, resolver);
|
|
157
|
+
return resolver;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/session.ts
|
|
161
|
+
async function verify(jwt, opts = {}) {
|
|
162
|
+
if (!jwt || typeof jwt !== "string") {
|
|
163
|
+
throw new MissingTokenError();
|
|
164
|
+
}
|
|
165
|
+
const hubUrl = opts.hubUrl ?? "https://openkeyai.com";
|
|
166
|
+
const resolver = getJwksResolver(hubUrl);
|
|
167
|
+
let payload;
|
|
168
|
+
try {
|
|
169
|
+
const result = await jwtVerify(jwt, resolver, {
|
|
170
|
+
issuer: "https://openkeyai.com",
|
|
171
|
+
algorithms: ["EdDSA"],
|
|
172
|
+
...opts.expectedAudience ? { audience: opts.expectedAudience } : {}
|
|
173
|
+
});
|
|
174
|
+
payload = result.payload;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (err instanceof errors.JWTExpired) {
|
|
177
|
+
throw new BadTokenError("Token expired.");
|
|
178
|
+
}
|
|
179
|
+
if (err instanceof errors.JWTClaimValidationFailed) {
|
|
180
|
+
throw new BadTokenError(`Claim validation failed: ${err.message}`);
|
|
181
|
+
}
|
|
182
|
+
if (err instanceof errors.JWSSignatureVerificationFailed) {
|
|
183
|
+
throw new BadTokenError("Signature did not verify.");
|
|
184
|
+
}
|
|
185
|
+
if (err instanceof errors.JWKSNoMatchingKey) {
|
|
186
|
+
throw new BadTokenError("Token kid is not in the hub's active JWKS.");
|
|
187
|
+
}
|
|
188
|
+
if (err instanceof errors.JOSEError) {
|
|
189
|
+
throw new BadTokenError(err.message);
|
|
190
|
+
}
|
|
191
|
+
throw new BadTokenError("Token verification failed.");
|
|
192
|
+
}
|
|
193
|
+
const sub = payload.sub;
|
|
194
|
+
const aud = payload.aud;
|
|
195
|
+
const scopes = payload.scopes;
|
|
196
|
+
const subscriptionActive = payload.subscription_active;
|
|
197
|
+
const jti = payload.jti;
|
|
198
|
+
const iat = payload.iat;
|
|
199
|
+
const exp = payload.exp;
|
|
200
|
+
if (typeof sub !== "string" || sub.length === 0) {
|
|
201
|
+
throw new BadTokenError("sub claim missing or invalid.");
|
|
202
|
+
}
|
|
203
|
+
if (typeof aud !== "string" || aud.length === 0) {
|
|
204
|
+
throw new BadTokenError("aud claim missing or invalid.");
|
|
205
|
+
}
|
|
206
|
+
if (!Array.isArray(scopes) || scopes.length === 0) {
|
|
207
|
+
throw new BadTokenError("scopes claim must be a non-empty array.");
|
|
208
|
+
}
|
|
209
|
+
if (typeof subscriptionActive !== "boolean") {
|
|
210
|
+
throw new BadTokenError("subscription_active claim must be boolean.");
|
|
211
|
+
}
|
|
212
|
+
if (typeof jti !== "string" || jti.length === 0) {
|
|
213
|
+
throw new BadTokenError("jti claim missing or invalid.");
|
|
214
|
+
}
|
|
215
|
+
if (typeof iat !== "number" || typeof exp !== "number") {
|
|
216
|
+
throw new BadTokenError("iat / exp claims missing or invalid.");
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
iss: "https://openkeyai.com",
|
|
220
|
+
sub,
|
|
221
|
+
aud,
|
|
222
|
+
scopes,
|
|
223
|
+
subscription_active: subscriptionActive,
|
|
224
|
+
iat,
|
|
225
|
+
exp,
|
|
226
|
+
jti
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/secure-key.ts
|
|
231
|
+
var SecureKey = class {
|
|
232
|
+
#plaintext;
|
|
233
|
+
/** Provider slug the key was fetched for. Public — not sensitive. */
|
|
234
|
+
provider;
|
|
235
|
+
/** @internal — tools should use `keys.get()`, never construct directly. */
|
|
236
|
+
constructor(plaintext, provider) {
|
|
237
|
+
this.#plaintext = plaintext;
|
|
238
|
+
this.provider = provider;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Pass the plaintext into a callback. Returns whatever the callback
|
|
242
|
+
* returns. After the callback resolves (or throws), the SecureKey is
|
|
243
|
+
* consumed — subsequent calls throw `SecureKeyConsumedError`.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* const k = await keys.get(jwt, "openai");
|
|
247
|
+
* const response = await k.use((apiKey) => fetch("...", {
|
|
248
|
+
* headers: { "Authorization": `Bearer ${apiKey}` },
|
|
249
|
+
* }));
|
|
250
|
+
*/
|
|
251
|
+
async use(fn) {
|
|
252
|
+
const plaintext = this.#plaintext;
|
|
253
|
+
if (plaintext === null) {
|
|
254
|
+
throw new SecureKeyConsumedError();
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
return await fn(plaintext);
|
|
258
|
+
} finally {
|
|
259
|
+
this.#plaintext = null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** Returns true once `use()` has been called and the reference cleared. */
|
|
263
|
+
get isConsumed() {
|
|
264
|
+
return this.#plaintext === null;
|
|
265
|
+
}
|
|
266
|
+
/** Always `[SecureKey]`. Never the underlying value. */
|
|
267
|
+
toString() {
|
|
268
|
+
return "[SecureKey]";
|
|
269
|
+
}
|
|
270
|
+
/** Always `[SecureKey]`. Catches `JSON.stringify` and friends. */
|
|
271
|
+
toJSON() {
|
|
272
|
+
return "[SecureKey]";
|
|
273
|
+
}
|
|
274
|
+
/** Catches `console.log` on Node and Workers (which use util.inspect). */
|
|
275
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
276
|
+
return "[SecureKey]";
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// src/keys.ts
|
|
281
|
+
async function get(jwt, provider, opts = {}) {
|
|
282
|
+
if (!jwt || typeof jwt !== "string") {
|
|
283
|
+
throw new BadTokenError("No JWT provided.");
|
|
284
|
+
}
|
|
285
|
+
if (!provider || typeof provider !== "string") {
|
|
286
|
+
throw new BadTokenError("Missing provider slug.");
|
|
287
|
+
}
|
|
288
|
+
let aud;
|
|
289
|
+
let scopes;
|
|
290
|
+
try {
|
|
291
|
+
const claims = decodeJwt(jwt);
|
|
292
|
+
aud = String(claims.aud ?? "");
|
|
293
|
+
scopes = claims.scopes;
|
|
294
|
+
} catch (err) {
|
|
295
|
+
throw new BadTokenError(
|
|
296
|
+
`JWT could not be decoded: ${err instanceof Error ? err.message : "unknown"}`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
if (!aud) {
|
|
300
|
+
throw new BadTokenError("JWT is missing the aud claim.");
|
|
301
|
+
}
|
|
302
|
+
if (Array.isArray(scopes) && !scopes.includes("keys.read")) {
|
|
303
|
+
throw new MissingScopeError("keys.read");
|
|
304
|
+
}
|
|
305
|
+
const hubUrl = opts.hubUrl ?? "https://openkeyai.com";
|
|
306
|
+
const url = new URL(
|
|
307
|
+
`/api/tools/${encodeURIComponent(aud)}/keys/${encodeURIComponent(provider)}`,
|
|
308
|
+
hubUrl
|
|
309
|
+
);
|
|
310
|
+
let response;
|
|
311
|
+
try {
|
|
312
|
+
response = await fetch(url.toString(), {
|
|
313
|
+
method: "GET",
|
|
314
|
+
headers: {
|
|
315
|
+
accept: "application/json",
|
|
316
|
+
authorization: `Bearer ${jwt}`
|
|
317
|
+
},
|
|
318
|
+
signal: opts.signal
|
|
319
|
+
});
|
|
320
|
+
} catch (err) {
|
|
321
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
322
|
+
throw err;
|
|
323
|
+
}
|
|
324
|
+
throw new NetworkError(err);
|
|
325
|
+
}
|
|
326
|
+
if (response.ok) {
|
|
327
|
+
let body;
|
|
328
|
+
try {
|
|
329
|
+
body = await response.json();
|
|
330
|
+
} catch (err) {
|
|
331
|
+
throw new InternalError(
|
|
332
|
+
response.status,
|
|
333
|
+
`Could not parse hub response body: ${err instanceof Error ? err.message : "unknown"}`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
if (typeof body.secret !== "string" || body.secret.length === 0) {
|
|
337
|
+
throw new InternalError(
|
|
338
|
+
response.status,
|
|
339
|
+
"Hub returned 200 OK but no secret in the body."
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return new SecureKey(body.secret, body.provider ?? provider);
|
|
343
|
+
}
|
|
344
|
+
let errorBody = null;
|
|
345
|
+
try {
|
|
346
|
+
errorBody = await response.json();
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
350
|
+
const retryAfter = retryAfterHeader ? parseInt(retryAfterHeader, 10) : 60;
|
|
351
|
+
throw errorFromResponse(response.status, errorBody, {
|
|
352
|
+
provider,
|
|
353
|
+
scopeNeeded: "keys.read",
|
|
354
|
+
retryAfter: Number.isFinite(retryAfter) ? retryAfter : 60
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/index.ts
|
|
359
|
+
var session = {
|
|
360
|
+
verify
|
|
361
|
+
};
|
|
362
|
+
var keys = {
|
|
363
|
+
get
|
|
364
|
+
};
|
|
365
|
+
var SDK_VERSION = "0.1.0";
|
|
366
|
+
|
|
367
|
+
export { BadTokenError, HubSdkError, InternalError, KeyNotFoundError, MissingScopeError, MissingTokenError, NetworkError, NotSubscribedError, ProviderNotGrantedError, RateLimitedError, SDK_VERSION, SecureKey, SecureKeyConsumedError, SubscriptionInactiveError, ToolNotFoundError, keys, session };
|
|
368
|
+
//# sourceMappingURL=index.js.map
|
|
369
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/_internal/jwks.ts","../src/session.ts","../src/secure-key.ts","../src/keys.ts","../src/index.ts"],"names":["joseErrors"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Typed errors.\n *\n * The error `code` strings are part of the FROZEN public contract — see the\n * key-fetch endpoint's documentation in\n * https://github.com/Scott-Builds-AI/hub/blob/main/docs/phases/05-tools-keyfetch.md\n *\n * Adding a new code is a minor-version bump. Renaming or removing one is a\n * major (with 60-day notice).\n *\n * Tools catch these to surface precise UX:\n *\n * try {\n * await keys.get(jwt, \"openai\");\n * } catch (e) {\n * if (e instanceof SubscriptionInactiveError) showPaywall();\n * else if (e instanceof RateLimitedError) showRetryToast(e.retryAfter);\n * else throw e;\n * }\n */\n\nexport type HubSdkErrorCode =\n | \"missing_token\"\n | \"bad_token\"\n | \"missing_scope\"\n | \"subscription_inactive\"\n | \"tool_not_found\"\n | \"not_subscribed\"\n | \"provider_not_granted\"\n | \"rate_limited\"\n | \"key_not_found\"\n | \"internal\"\n | \"network\"\n | \"secure_key_consumed\";\n\n/** Base class for every typed error in the SDK. */\nexport class HubSdkError extends Error {\n readonly code: HubSdkErrorCode;\n /** HTTP status from the hub, when applicable. 0 for client-only errors. */\n readonly status: number;\n\n constructor(code: HubSdkErrorCode, message: string, status = 0) {\n super(message);\n this.name = \"HubSdkError\";\n this.code = code;\n this.status = status;\n }\n}\n\n// ─── Wire-level errors (returned by the hub) ────────────────────────────────\n\nexport class MissingTokenError extends HubSdkError {\n constructor() {\n super(\"missing_token\", \"No Authorization token was provided.\", 401);\n this.name = \"MissingTokenError\";\n }\n}\n\nexport class BadTokenError extends HubSdkError {\n constructor(message = \"Token signature / issuer / audience / expiry check failed.\") {\n super(\"bad_token\", message, 401);\n this.name = \"BadTokenError\";\n }\n}\n\nexport class MissingScopeError extends HubSdkError {\n constructor(needed: string) {\n super(\"missing_scope\", `Token is missing the required scope: ${needed}.`, 403);\n this.name = \"MissingScopeError\";\n }\n}\n\nexport class SubscriptionInactiveError extends HubSdkError {\n constructor() {\n super(\n \"subscription_inactive\",\n \"The user's subscription is not active. Surface a paywall — don't retry.\",\n 402,\n );\n this.name = \"SubscriptionInactiveError\";\n }\n}\n\nexport class ToolNotFoundError extends HubSdkError {\n constructor() {\n super(\"tool_not_found\", \"Tool unknown or not approved.\", 404);\n this.name = \"ToolNotFoundError\";\n }\n}\n\nexport class NotSubscribedError extends HubSdkError {\n constructor() {\n super(\n \"not_subscribed\",\n \"The user has not enabled this tool in their hub settings.\",\n 403,\n );\n this.name = \"NotSubscribedError\";\n }\n}\n\nexport class ProviderNotGrantedError extends HubSdkError {\n constructor(provider: string) {\n super(\n \"provider_not_granted\",\n `User has not granted this tool access to the ${provider} provider.`,\n 403,\n );\n this.name = \"ProviderNotGrantedError\";\n }\n}\n\nexport class RateLimitedError extends HubSdkError {\n readonly retryAfter: number;\n constructor(retryAfter: number) {\n super(\n \"rate_limited\",\n `Rate limit hit. Retry after ${retryAfter} seconds.`,\n 429,\n );\n this.name = \"RateLimitedError\";\n this.retryAfter = retryAfter;\n }\n}\n\nexport class KeyNotFoundError extends HubSdkError {\n constructor(provider: string) {\n super(\n \"key_not_found\",\n `No ${provider} key configured for this user. The tool should prompt the user to add one.`,\n 404,\n );\n this.name = \"KeyNotFoundError\";\n }\n}\n\nexport class InternalError extends HubSdkError {\n constructor(status: number, message = \"The hub returned an internal error.\") {\n super(\"internal\", message, status);\n this.name = \"InternalError\";\n }\n}\n\n// ─── Client-only errors ──────────────────────────────────────────────────────\n\nexport class NetworkError extends HubSdkError {\n constructor(cause: unknown) {\n super(\n \"network\",\n `Could not reach the hub: ${cause instanceof Error ? cause.message : String(cause)}`,\n 0,\n );\n this.name = \"NetworkError\";\n this.cause = cause;\n }\n}\n\nexport class SecureKeyConsumedError extends HubSdkError {\n constructor() {\n super(\n \"secure_key_consumed\",\n \"SecureKey has already been used. Each instance is single-use; call keys.get() again to fetch a fresh one.\",\n 0,\n );\n this.name = \"SecureKeyConsumedError\";\n }\n}\n\n// ─── Wire → error mapping ────────────────────────────────────────────────────\n\n/**\n * Build the right HubSdkError subclass from a hub JSON error response. Used\n * by every fetch wrapper so the typed errors stay consistent.\n */\nexport function errorFromResponse(\n status: number,\n body: { error?: string } | null,\n context: { provider?: string; scopeNeeded?: string; retryAfter?: number },\n): HubSdkError {\n const code = body?.error ?? \"internal\";\n switch (code) {\n case \"missing_token\":\n return new MissingTokenError();\n case \"bad_token\":\n return new BadTokenError();\n case \"missing_scope\":\n return new MissingScopeError(context.scopeNeeded ?? \"unknown\");\n case \"subscription_inactive\":\n return new SubscriptionInactiveError();\n case \"tool_not_found\":\n return new ToolNotFoundError();\n case \"not_subscribed\":\n return new NotSubscribedError();\n case \"provider_not_granted\":\n return new ProviderNotGrantedError(context.provider ?? \"unknown\");\n case \"rate_limited\":\n return new RateLimitedError(context.retryAfter ?? 60);\n case \"key_not_found\":\n return new KeyNotFoundError(context.provider ?? \"unknown\");\n case \"internal\":\n default:\n return new InternalError(status);\n }\n}\n","import { createRemoteJWKSet, type JWTVerifyGetKey } from \"jose\";\n\n/**\n * Per-hub-URL JWKS cache.\n *\n * Each call to `keys.get` / `session.verify` needs a key-resolver\n * (`jose.JWTVerifyGetKey`) to feed into `jwtVerify`. Building a fresh\n * resolver per request would re-fetch the JWKS document every time.\n * `createRemoteJWKSet` handles caching internally, but only if we hand\n * back the SAME resolver instance for the same URL.\n *\n * Cache key is the hub URL because the same SDK build might be used by\n * tools that point at staging vs production hubs (different keys, different\n * kids).\n *\n * The cache has no eviction — there's at most a handful of distinct hub\n * URLs in any sane setup. If a tool author somehow constructs thousands of\n * URL variants this would leak memory; that's not a realistic path.\n */\nconst resolvers = new Map<string, JWTVerifyGetKey>();\n\nexport function getJwksResolver(hubUrl: string): JWTVerifyGetKey {\n const cached = resolvers.get(hubUrl);\n if (cached) return cached;\n const url = new URL(\"/.well-known/jwks.json\", hubUrl);\n // jose's createRemoteJWKSet:\n // - fetches lazily on the first verify\n // - caches for `cacheMaxAge` (default 10 minutes)\n // - revalidates after `cooldownDuration` (default 30s)\n // - re-fetches automatically when a verify hits an unknown kid (rotation)\n const resolver = createRemoteJWKSet(url, {\n // Respect the hub's stated cache window (5 min strong + 10 min SWR).\n cacheMaxAge: 5 * 60 * 1000,\n cooldownDuration: 30 * 1000,\n });\n resolvers.set(hubUrl, resolver);\n return resolver;\n}\n\n/** Test helper — drops the cache between cases. Not part of public API. */\nexport function _resetJwksCacheForTests(): void {\n resolvers.clear();\n}\n","import { jwtVerify, errors as joseErrors } from \"jose\";\nimport { BadTokenError, MissingTokenError } from \"./errors\";\nimport { getJwksResolver } from \"./_internal/jwks\";\nimport type { HubCallOptions, ToolJwtClaims, ToolJwtScope } from \"./types\";\n\n/**\n * `session.verify(jwt, opts?)`\n *\n * Verifies a hub-issued JWT against the hub's public JWKS, then validates\n * the claim shape (`iss`, `sub`, `aud`, `scopes`, `subscription_active`).\n *\n * Returns the typed claims on success.\n * Throws `BadTokenError` on any failure — signature, expiry, issuer,\n * audience, missing/malformed custom claims.\n * Throws `MissingTokenError` if `jwt` is empty / null.\n *\n * The verifier:\n * - fetches `${hubUrl}/.well-known/jwks.json` (cached per hub URL)\n * - re-fetches on `kid` rotation (jose handles this transparently)\n * - DOES NOT validate `aud` here — the caller specifies which audience\n * they expect (the tool's own slug, OR a wildcard for tools that\n * legitimately accept multiple audiences). `keys.get` validates\n * audience against the tool slug derived from the route.\n *\n * @param jwt The bearer token string (no `Bearer ` prefix).\n * @param opts.hubUrl override the hub root (default: `https://openkeyai.com`)\n * @param opts.expectedAudience tool slug the token must be addressed to.\n * When omitted, audience is not checked here — the caller MUST do it\n * themselves before granting any scope-derived privilege.\n */\nexport async function verify(\n jwt: string,\n opts: HubCallOptions & { expectedAudience?: string } = {},\n): Promise<ToolJwtClaims> {\n if (!jwt || typeof jwt !== \"string\") {\n throw new MissingTokenError();\n }\n\n const hubUrl = opts.hubUrl ?? \"https://openkeyai.com\";\n const resolver = getJwksResolver(hubUrl);\n\n let payload: Record<string, unknown>;\n try {\n const result = await jwtVerify(jwt, resolver, {\n issuer: \"https://openkeyai.com\",\n algorithms: [\"EdDSA\"],\n ...(opts.expectedAudience ? { audience: opts.expectedAudience } : {}),\n });\n payload = result.payload as Record<string, unknown>;\n } catch (err) {\n if (err instanceof joseErrors.JWTExpired) {\n throw new BadTokenError(\"Token expired.\");\n }\n if (err instanceof joseErrors.JWTClaimValidationFailed) {\n throw new BadTokenError(`Claim validation failed: ${err.message}`);\n }\n if (err instanceof joseErrors.JWSSignatureVerificationFailed) {\n throw new BadTokenError(\"Signature did not verify.\");\n }\n if (err instanceof joseErrors.JWKSNoMatchingKey) {\n throw new BadTokenError(\"Token kid is not in the hub's active JWKS.\");\n }\n if (err instanceof joseErrors.JOSEError) {\n throw new BadTokenError(err.message);\n }\n // Network / unknown — surface as BadToken too rather than leak details.\n throw new BadTokenError(\"Token verification failed.\");\n }\n\n // Custom-claim shape validation. `jose` only validates standard claims.\n const sub = payload.sub;\n const aud = payload.aud;\n const scopes = payload.scopes;\n const subscriptionActive = payload.subscription_active;\n const jti = payload.jti;\n const iat = payload.iat;\n const exp = payload.exp;\n\n if (typeof sub !== \"string\" || sub.length === 0) {\n throw new BadTokenError(\"sub claim missing or invalid.\");\n }\n if (typeof aud !== \"string\" || aud.length === 0) {\n throw new BadTokenError(\"aud claim missing or invalid.\");\n }\n if (!Array.isArray(scopes) || scopes.length === 0) {\n throw new BadTokenError(\"scopes claim must be a non-empty array.\");\n }\n if (typeof subscriptionActive !== \"boolean\") {\n throw new BadTokenError(\"subscription_active claim must be boolean.\");\n }\n if (typeof jti !== \"string\" || jti.length === 0) {\n throw new BadTokenError(\"jti claim missing or invalid.\");\n }\n if (typeof iat !== \"number\" || typeof exp !== \"number\") {\n throw new BadTokenError(\"iat / exp claims missing or invalid.\");\n }\n\n return {\n iss: \"https://openkeyai.com\",\n sub,\n aud,\n scopes: scopes as ToolJwtScope[],\n subscription_active: subscriptionActive,\n iat,\n exp,\n jti,\n };\n}\n","import { SecureKeyConsumedError } from \"./errors\";\n\n/**\n * SecureKey — the only way the SDK hands a plaintext credential to a tool.\n *\n * Design goals (per docs/SECURITY.md in the hub repo):\n *\n * 1. The plaintext is reachable from EXACTLY ONE place: inside a callback\n * passed to `use(fn)`. Outside that callback, no public method or\n * property returns the value.\n *\n * 2. After the callback runs (or throws), the held reference is set to\n * null. Subsequent `use()` calls throw `SecureKeyConsumedError`. The\n * pattern is one-shot — get a fresh SecureKey for the next request.\n *\n * 3. `JSON.stringify`, `toString`, `console.log` (via util.inspect), and\n * template-literal coercion all return the literal string\n * `\"[SecureKey]\"` — never the underlying value, even by accident.\n *\n * #plaintext is a true private field (Stage 3 syntax) so it doesn't appear\n * in `Object.keys()`, isn't accessible via bracket-notation, and is not\n * enumerable for serialisation. The frozen overrides below cover the\n * coercion paths.\n *\n * What we deliberately do NOT do:\n *\n * - WeakRef + FinalizationRegistry. They're observable from the same realm,\n * and we have no useful action to take on GC. Single-use + manual null\n * is the simpler, more deterministic guarantee.\n *\n * - Buffer.alloc + .fill(0) \"zeroising\". V8 and modern runtimes can keep\n * copies of strings in cache; we can't truly zeroise from JS. The\n * useful guarantee we CAN give is reference-clearing, which we do.\n */\nexport class SecureKey {\n #plaintext: string | null;\n\n /** Provider slug the key was fetched for. Public — not sensitive. */\n readonly provider: string;\n\n /** @internal — tools should use `keys.get()`, never construct directly. */\n constructor(plaintext: string, provider: string) {\n this.#plaintext = plaintext;\n this.provider = provider;\n }\n\n /**\n * Pass the plaintext into a callback. Returns whatever the callback\n * returns. After the callback resolves (or throws), the SecureKey is\n * consumed — subsequent calls throw `SecureKeyConsumedError`.\n *\n * @example\n * const k = await keys.get(jwt, \"openai\");\n * const response = await k.use((apiKey) => fetch(\"...\", {\n * headers: { \"Authorization\": `Bearer ${apiKey}` },\n * }));\n */\n async use<T>(fn: (plaintext: string) => T | Promise<T>): Promise<T> {\n const plaintext = this.#plaintext;\n if (plaintext === null) {\n throw new SecureKeyConsumedError();\n }\n try {\n return await fn(plaintext);\n } finally {\n // Always clear the held reference, even if `fn` threw. The single\n // legitimate consumer already had the value during the callback;\n // anything after that is leakage we don't want to enable.\n this.#plaintext = null;\n }\n }\n\n /** Returns true once `use()` has been called and the reference cleared. */\n get isConsumed(): boolean {\n return this.#plaintext === null;\n }\n\n /** Always `[SecureKey]`. Never the underlying value. */\n toString(): string {\n return \"[SecureKey]\";\n }\n\n /** Always `[SecureKey]`. Catches `JSON.stringify` and friends. */\n toJSON(): string {\n return \"[SecureKey]\";\n }\n\n /** Catches `console.log` on Node and Workers (which use util.inspect). */\n [Symbol.for(\"nodejs.util.inspect.custom\")](): string {\n return \"[SecureKey]\";\n }\n}\n","import { decodeJwt } from \"jose\";\nimport {\n BadTokenError,\n InternalError,\n MissingScopeError,\n NetworkError,\n errorFromResponse,\n} from \"./errors\";\nimport { SecureKey } from \"./secure-key\";\nimport type { HubCallOptions, ProviderSlug } from \"./types\";\n\n/**\n * `keys.get(jwt, provider, opts?)`\n *\n * Fetches a credential for the user identified by `jwt` and returns a\n * single-use SecureKey. The hub:\n *\n * 1. Verifies the JWT (signature + iss + aud + exp)\n * 2. Confirms `keys.read` scope, active subscription, and that the tool +\n * user are both subscribed to this provider\n * 3. Decrypts the user's stored API key via KMS\n * 4. Returns the plaintext, logged to the audit trail\n *\n * On the client side we:\n * - Decode (no verify) the JWT locally to read the `aud` claim — that's\n * the tool slug the hub will scope the lookup against\n * - Pre-check the `keys.read` scope so we can return a typed error\n * without a round-trip\n * - Issue the GET, map the hub's frozen error codes to typed exceptions\n * - Wrap the plaintext in a SecureKey and return\n *\n * IMPORTANT: a SecureKey is single-use. Don't call `.use()` more than once;\n * call `keys.get` again for the next request. The audit trail records every\n * fetch.\n */\nexport async function get(\n jwt: string,\n provider: ProviderSlug,\n opts: HubCallOptions = {},\n): Promise<SecureKey> {\n if (!jwt || typeof jwt !== \"string\") {\n throw new BadTokenError(\"No JWT provided.\");\n }\n if (!provider || typeof provider !== \"string\") {\n throw new BadTokenError(\"Missing provider slug.\");\n }\n\n // Decode (no verify) to learn the audience — that's the tool slug for the\n // route. The hub will re-verify; we don't need to here.\n let aud: string;\n let scopes: unknown;\n try {\n const claims = decodeJwt(jwt);\n aud = String(claims.aud ?? \"\");\n scopes = claims.scopes;\n } catch (err) {\n throw new BadTokenError(\n `JWT could not be decoded: ${err instanceof Error ? err.message : \"unknown\"}`,\n );\n }\n if (!aud) {\n throw new BadTokenError(\"JWT is missing the aud claim.\");\n }\n\n // Local scope pre-check. Cheap, and lets the tool surface the right UX\n // without a network round-trip when the scope is just missing.\n if (Array.isArray(scopes) && !scopes.includes(\"keys.read\")) {\n throw new MissingScopeError(\"keys.read\");\n }\n\n const hubUrl = opts.hubUrl ?? \"https://openkeyai.com\";\n const url = new URL(\n `/api/tools/${encodeURIComponent(aud)}/keys/${encodeURIComponent(provider)}`,\n hubUrl,\n );\n\n let response: Response;\n try {\n response = await fetch(url.toString(), {\n method: \"GET\",\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${jwt}`,\n },\n signal: opts.signal,\n });\n } catch (err) {\n if (err instanceof DOMException && err.name === \"AbortError\") {\n throw err; // propagate as-is so callers can detect abort.\n }\n throw new NetworkError(err);\n }\n\n if (response.ok) {\n let body: { provider?: string; secret?: string };\n try {\n body = (await response.json()) as { provider?: string; secret?: string };\n } catch (err) {\n throw new InternalError(\n response.status,\n `Could not parse hub response body: ${err instanceof Error ? err.message : \"unknown\"}`,\n );\n }\n if (typeof body.secret !== \"string\" || body.secret.length === 0) {\n throw new InternalError(\n response.status,\n \"Hub returned 200 OK but no secret in the body.\",\n );\n }\n return new SecureKey(body.secret, body.provider ?? provider);\n }\n\n // Error path — let the typed-error builder do the right thing.\n let errorBody: { error?: string } | null = null;\n try {\n errorBody = (await response.json()) as { error?: string };\n } catch {\n // Body wasn't JSON. The status alone is informative; carry on with null.\n }\n\n const retryAfterHeader = response.headers.get(\"retry-after\");\n const retryAfter = retryAfterHeader ? parseInt(retryAfterHeader, 10) : 60;\n\n throw errorFromResponse(response.status, errorBody, {\n provider,\n scopeNeeded: \"keys.read\",\n retryAfter: Number.isFinite(retryAfter) ? retryAfter : 60,\n });\n}\n","/**\n * `@openkeyai/sdk` — public entry.\n *\n * Tools install this and import only from the package root:\n *\n * import { session, keys, SecureKey, SubscriptionInactiveError } from \"@openkeyai/sdk\";\n *\n * The five-module surface (session / keys / user / billing / webhooks) is\n * defined in\n * https://github.com/Scott-Builds-AI/hub/blob/main/docs/TOOL_SDK.md\n *\n * Status by module in 0.1.0:\n * - session.verify ✓\n * - keys.get → SecureKey ✓\n * - user — deferred until the hub ships /api/me (issue TBD)\n * - billing — deferred until the hub ships /api/billing/status\n * - webhooks — deferred until Phase 16 (the hub's webhook delivery layer)\n *\n * Internal helpers live under `_internal/`. They are NOT part of the\n * public API and may change without notice — the tool-manifest scanner\n * (Phase 9) treats `@openkeyai/sdk/_internal` imports as a CI failure.\n */\n\nimport { verify } from \"./session\";\nimport { get } from \"./keys\";\n\n/** session module — JWT verification + (future) refresh. */\nexport const session = {\n verify,\n} as const;\n\n/** keys module — credential fetch returning a single-use SecureKey. */\nexport const keys = {\n get,\n} as const;\n\n// Re-exports — the SecureKey class + every typed error, so tools can\n// `instanceof` check without reaching into submodules.\nexport { SecureKey } from \"./secure-key\";\nexport {\n HubSdkError,\n MissingTokenError,\n BadTokenError,\n MissingScopeError,\n SubscriptionInactiveError,\n ToolNotFoundError,\n NotSubscribedError,\n ProviderNotGrantedError,\n RateLimitedError,\n KeyNotFoundError,\n InternalError,\n NetworkError,\n SecureKeyConsumedError,\n} from \"./errors\";\nexport type { HubSdkErrorCode } from \"./errors\";\n\n// Type re-exports for tools that want to annotate their own helpers.\nexport type {\n ToolJwtClaims,\n ToolJwtScope,\n ProviderSlug,\n HubCallOptions,\n} from \"./types\";\n\n/** Bumped on each release. Tools log this on boot. */\nexport const SDK_VERSION = \"0.1.0\";\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openkeyai/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The SDK every OpenKey AI tool installs. SecureKey key-fetch, JWT verify, webhooks.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Scott Goodwin",
|
|
7
|
+
"homepage": "https://openkeyai.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Scott-Builds-AI/hub-sdk.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Scott-Builds-AI/hub-sdk/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.cjs",
|
|
23
|
+
"types": "./dist/index.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"tsup": "^8.3.0",
|
|
33
|
+
"typescript": "^5.6.0",
|
|
34
|
+
"vitest": "^2.1.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"jose": "^6.2.3"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"dev": "tsup --watch",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:watch": "vitest"
|
|
51
|
+
}
|
|
52
|
+
}
|