@silkweave/auth 1.3.0 → 1.3.2

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/build/index.js CHANGED
@@ -6,6 +6,8 @@ var AuthError = class extends Error {
6
6
  this.statusCode = statusCode;
7
7
  this.name = "AuthError";
8
8
  }
9
+ code;
10
+ statusCode;
9
11
  };
10
12
  function invalidToken(message = "Invalid token") {
11
13
  return new AuthError(message, "invalid_token", 401);
@@ -13,9 +15,6 @@ function invalidToken(message = "Invalid token") {
13
15
  function insufficientScope(message = "Insufficient scope") {
14
16
  return new AuthError(message, "insufficient_scope", 403);
15
17
  }
16
- function missingToken(message = "No authorization provided") {
17
- return new AuthError(message, "missing_token", 401);
18
- }
19
18
 
20
19
  // src/extract.ts
21
20
  function extractBearerToken(header) {
@@ -48,6 +47,10 @@ function generateProtectedResourceMetadata(resourceUrl, authorizationServers) {
48
47
  };
49
48
  }
50
49
 
50
+ // src/provider/proxy.ts
51
+ import { randomBytes, randomUUID } from "crypto";
52
+ import { SignJWT, jwtVerify } from "jose";
53
+
51
54
  // src/store/memory-store.ts
52
55
  function withTtl(map, key) {
53
56
  const item = map.get(key);
@@ -120,10 +123,6 @@ function createMemoryStore() {
120
123
  };
121
124
  }
122
125
 
123
- // src/provider/proxy.ts
124
- import { randomBytes, randomUUID } from "crypto";
125
- import { SignJWT, jwtVerify } from "jose";
126
-
127
126
  // src/provider/uri.ts
128
127
  function matchRedirectUri(uri, patterns) {
129
128
  return patterns.some((pattern) => {
@@ -142,6 +141,233 @@ var CORS_HEADERS = {
142
141
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
143
142
  "Access-Control-Allow-Headers": "*"
144
143
  };
144
+ function generateCode() {
145
+ return randomBytes(32).toString("base64url");
146
+ }
147
+ async function generatePkce() {
148
+ const verifier = randomBytes(32).toString("base64url");
149
+ const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
150
+ const challenge = Buffer.from(hash).toString("base64url");
151
+ return { verifier, challenge };
152
+ }
153
+ async function verifyPkce(verifier, challenge) {
154
+ const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
155
+ const computed = Buffer.from(hash).toString("base64url");
156
+ return computed === challenge;
157
+ }
158
+ function jsonResponse(status, body, headers = {}) {
159
+ return {
160
+ status,
161
+ headers: { "Content-Type": "application/json", ...CORS_HEADERS, ...headers },
162
+ body
163
+ };
164
+ }
165
+ function redirectResponse(url) {
166
+ return {
167
+ status: 302,
168
+ headers: { Location: url, ...CORS_HEADERS },
169
+ body: void 0
170
+ };
171
+ }
172
+ function errorResponse(status, error, description) {
173
+ return jsonResponse(status, { error, error_description: description });
174
+ }
175
+ async function signAccessToken(key, opts) {
176
+ const now = Math.floor(Date.now() / 1e3);
177
+ return new SignJWT({ scope: opts.scopes.join(" "), email: opts.email }).setProtectedHeader({ alg: "HS256" }).setSubject(opts.sub ?? opts.email ?? opts.clientId).setIssuer(opts.issuer).setAudience(opts.issuer).setIssuedAt(now).setExpirationTime(now + opts.ttl).sign(key);
178
+ }
179
+ async function handleRegister(req, store, allowedRedirectUris) {
180
+ const body = req.body;
181
+ if (!body) {
182
+ return errorResponse(400, "invalid_request", "Missing request body");
183
+ }
184
+ const redirectUris = body.redirect_uris;
185
+ if (!redirectUris) {
186
+ return errorResponse(400, "invalid_request", "redirect_uris is required");
187
+ }
188
+ let uris;
189
+ try {
190
+ uris = typeof redirectUris === "string" ? JSON.parse(redirectUris) : redirectUris;
191
+ } catch {
192
+ uris = [redirectUris];
193
+ }
194
+ if (!Array.isArray(uris) || uris.length === 0) {
195
+ return errorResponse(400, "invalid_request", "redirect_uris must be a non-empty array");
196
+ }
197
+ for (const uri of uris) {
198
+ if (!matchRedirectUri(uri, allowedRedirectUris)) {
199
+ return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
200
+ }
201
+ }
202
+ const clientId = randomUUID();
203
+ const clientSecret = randomBytes(32).toString("base64url");
204
+ const registration = {
205
+ clientId,
206
+ clientSecret,
207
+ redirectUris: uris,
208
+ clientName: body.client_name,
209
+ createdAt: Date.now() / 1e3
210
+ };
211
+ await store.saveClient(clientId, registration);
212
+ return jsonResponse(201, {
213
+ client_id: clientId,
214
+ client_secret: clientSecret,
215
+ redirect_uris: uris,
216
+ client_name: body.client_name,
217
+ token_endpoint_auth_method: "none"
218
+ });
219
+ }
220
+ async function resolveClient(clientId, store, allowedRedirectUris) {
221
+ const existing = await store.getClient(clientId);
222
+ if (existing) {
223
+ return existing;
224
+ }
225
+ if (!clientId.startsWith("https://")) {
226
+ return errorResponse(400, "invalid_client", "Unknown client_id");
227
+ }
228
+ try {
229
+ const metaRes = await fetch(clientId);
230
+ if (!metaRes.ok) {
231
+ return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
232
+ }
233
+ const meta = await metaRes.json();
234
+ const metaRedirectUris = meta.redirect_uris;
235
+ if (!Array.isArray(metaRedirectUris) || metaRedirectUris.length === 0) {
236
+ return errorResponse(400, "invalid_client", "Client metadata must include redirect_uris");
237
+ }
238
+ for (const uri of metaRedirectUris) {
239
+ if (!matchRedirectUri(uri, allowedRedirectUris)) {
240
+ return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
241
+ }
242
+ }
243
+ const client = {
244
+ clientId,
245
+ clientSecret: "",
246
+ redirectUris: metaRedirectUris,
247
+ clientName: meta.client_name,
248
+ createdAt: Date.now() / 1e3
249
+ };
250
+ await store.saveClient(clientId, client);
251
+ return client;
252
+ } catch {
253
+ return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
254
+ }
255
+ }
256
+ async function exchangeUpstreamCode(code, config, callbackPath, pkceVerifier) {
257
+ const tokenBody = new URLSearchParams({
258
+ grant_type: "authorization_code",
259
+ code,
260
+ redirect_uri: `${config.resourceUrl}${callbackPath}`,
261
+ client_id: config.clientId,
262
+ client_secret: config.clientSecret,
263
+ code_verifier: pkceVerifier
264
+ });
265
+ const tokenResponse = await fetch(config.tokenUrl, {
266
+ method: "POST",
267
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
268
+ body: tokenBody.toString()
269
+ });
270
+ if (!tokenResponse.ok) {
271
+ const errorBody = await tokenResponse.text();
272
+ console.error("Upstream token exchange failed:", errorBody);
273
+ return errorResponse(502, "upstream_error", "Failed to exchange code with upstream provider");
274
+ }
275
+ const tokens = await tokenResponse.json();
276
+ return {
277
+ accessToken: tokens.access_token,
278
+ idToken: tokens.id_token
279
+ };
280
+ }
281
+ async function fetchUserinfo(userinfoUrl, accessToken) {
282
+ if (!userinfoUrl || !accessToken) {
283
+ return {};
284
+ }
285
+ try {
286
+ const res = await fetch(userinfoUrl, {
287
+ headers: { Authorization: `Bearer ${accessToken}` }
288
+ });
289
+ if (res.ok) {
290
+ const userinfo = await res.json();
291
+ return { email: userinfo.email, sub: userinfo.sub };
292
+ }
293
+ } catch {
294
+ }
295
+ return {};
296
+ }
297
+ async function handleRefreshToken(body, store, getSigningKey, config) {
298
+ const refreshToken = body.refresh_token;
299
+ if (!refreshToken) {
300
+ return errorResponse(400, "invalid_request", "Missing refresh_token");
301
+ }
302
+ const tokenData = await store.getRefreshToken(refreshToken);
303
+ if (!tokenData) {
304
+ return errorResponse(400, "invalid_grant", "Invalid or expired refresh token");
305
+ }
306
+ const key = await getSigningKey();
307
+ const accessToken = await signAccessToken(key, {
308
+ scopes: tokenData.scopes,
309
+ email: tokenData.email,
310
+ sub: tokenData.sub,
311
+ clientId: tokenData.clientId,
312
+ issuer: config.resourceUrl,
313
+ ttl: config.tokenTtl
314
+ });
315
+ return jsonResponse(200, {
316
+ access_token: accessToken,
317
+ token_type: "Bearer",
318
+ expires_in: config.tokenTtl,
319
+ scope: tokenData.scopes.join(" "),
320
+ refresh_token: refreshToken
321
+ });
322
+ }
323
+ async function handleAuthorizationCode(body, store, getSigningKey, config) {
324
+ const code = body.code;
325
+ const redirectUri = body.redirect_uri;
326
+ const clientId = body.client_id;
327
+ const codeVerifier = body.code_verifier;
328
+ if (!code || !codeVerifier || !clientId) {
329
+ return errorResponse(400, "invalid_request", "Missing required parameters");
330
+ }
331
+ const authCode = await store.getAuthCode(code);
332
+ if (!authCode) {
333
+ return errorResponse(400, "invalid_grant", "Invalid or expired authorization code");
334
+ }
335
+ await store.deleteAuthCode(code);
336
+ if (authCode.clientId !== clientId) {
337
+ return errorResponse(400, "invalid_grant", "client_id mismatch");
338
+ }
339
+ if (redirectUri && authCode.redirectUri !== redirectUri) {
340
+ return errorResponse(400, "invalid_grant", "redirect_uri mismatch");
341
+ }
342
+ const pkceValid = await verifyPkce(codeVerifier, authCode.codeChallenge);
343
+ if (!pkceValid) {
344
+ return errorResponse(400, "invalid_grant", "PKCE verification failed");
345
+ }
346
+ const key = await getSigningKey();
347
+ const accessToken = await signAccessToken(key, {
348
+ scopes: authCode.scopes,
349
+ email: authCode.email,
350
+ sub: authCode.sub,
351
+ clientId: authCode.clientId,
352
+ issuer: config.resourceUrl,
353
+ ttl: config.tokenTtl
354
+ });
355
+ const refreshToken = randomBytes(32).toString("base64url");
356
+ await store.saveRefreshToken(refreshToken, {
357
+ clientId: authCode.clientId,
358
+ scopes: authCode.scopes,
359
+ email: authCode.email,
360
+ sub: authCode.sub,
361
+ expiresAt: Math.floor(Date.now() / 1e3) + 30 * 24 * 3600
362
+ });
363
+ return jsonResponse(200, {
364
+ access_token: accessToken,
365
+ token_type: "Bearer",
366
+ expires_in: config.tokenTtl,
367
+ scope: authCode.scopes.join(" "),
368
+ refresh_token: refreshToken
369
+ });
370
+ }
145
371
  function createOAuthProxy(config) {
146
372
  const store = config.store ?? createMemoryStore();
147
373
  const callbackPath = config.callbackPath ?? "/auth/callback";
@@ -159,38 +385,7 @@ function createOAuthProxy(config) {
159
385
  }
160
386
  return signingKey;
161
387
  }
162
- function generateCode() {
163
- return randomBytes(32).toString("base64url");
164
- }
165
- async function generatePkce() {
166
- const verifier = randomBytes(32).toString("base64url");
167
- const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
168
- const challenge = Buffer.from(hash).toString("base64url");
169
- return { verifier, challenge };
170
- }
171
- async function verifyPkce(verifier, challenge) {
172
- const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
173
- const computed = Buffer.from(hash).toString("base64url");
174
- return computed === challenge;
175
- }
176
- function jsonResponse(status, body, headers = {}) {
177
- return {
178
- status,
179
- headers: { "Content-Type": "application/json", ...CORS_HEADERS, ...headers },
180
- body
181
- };
182
- }
183
- function redirectResponse(url) {
184
- return {
185
- status: 302,
186
- headers: { Location: url, ...CORS_HEADERS },
187
- body: void 0
188
- };
189
- }
190
- function errorResponse(status, error, description) {
191
- return jsonResponse(status, { error, error_description: description });
192
- }
193
- const provider = {
388
+ return {
194
389
  metadata() {
195
390
  return jsonResponse(200, {
196
391
  issuer: config.resourceUrl,
@@ -204,46 +399,8 @@ function createOAuthProxy(config) {
204
399
  scopes_supported: scopes
205
400
  }, { "Cache-Control": "max-age=3600" });
206
401
  },
207
- async register(req) {
208
- const body = req.body;
209
- if (!body) {
210
- return errorResponse(400, "invalid_request", "Missing request body");
211
- }
212
- const redirectUris = body.redirect_uris;
213
- if (!redirectUris) {
214
- return errorResponse(400, "invalid_request", "redirect_uris is required");
215
- }
216
- let uris;
217
- try {
218
- uris = typeof redirectUris === "string" ? JSON.parse(redirectUris) : redirectUris;
219
- } catch {
220
- uris = [redirectUris];
221
- }
222
- if (!Array.isArray(uris) || uris.length === 0) {
223
- return errorResponse(400, "invalid_request", "redirect_uris must be a non-empty array");
224
- }
225
- for (const uri of uris) {
226
- if (!matchRedirectUri(uri, config.redirectUris)) {
227
- return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
228
- }
229
- }
230
- const clientId = randomUUID();
231
- const clientSecret = randomBytes(32).toString("base64url");
232
- const registration = {
233
- clientId,
234
- clientSecret,
235
- redirectUris: uris,
236
- clientName: body.client_name,
237
- createdAt: Date.now() / 1e3
238
- };
239
- await store.saveClient(clientId, registration);
240
- return jsonResponse(201, {
241
- client_id: clientId,
242
- client_secret: clientSecret,
243
- redirect_uris: uris,
244
- client_name: body.client_name,
245
- token_endpoint_auth_method: "none"
246
- });
402
+ register(req) {
403
+ return handleRegister(req, store, config.redirectUris);
247
404
  },
248
405
  async authorize(req) {
249
406
  const params = req.url.searchParams;
@@ -267,38 +424,11 @@ function createOAuthProxy(config) {
267
424
  if (!codeChallenge || codeChallengeMethod !== "S256") {
268
425
  return errorResponse(400, "invalid_request", "PKCE with S256 is required");
269
426
  }
270
- let client = await store.getClient(clientId);
271
- if (!client && clientId.startsWith("https://")) {
272
- try {
273
- const metaRes = await fetch(clientId);
274
- if (!metaRes.ok) {
275
- return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
276
- }
277
- const meta = await metaRes.json();
278
- const metaRedirectUris = meta.redirect_uris;
279
- if (!Array.isArray(metaRedirectUris) || metaRedirectUris.length === 0) {
280
- return errorResponse(400, "invalid_client", "Client metadata must include redirect_uris");
281
- }
282
- for (const uri of metaRedirectUris) {
283
- if (!matchRedirectUri(uri, config.redirectUris)) {
284
- return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
285
- }
286
- }
287
- client = {
288
- clientId,
289
- clientSecret: "",
290
- redirectUris: metaRedirectUris,
291
- clientName: meta.client_name,
292
- createdAt: Date.now() / 1e3
293
- };
294
- await store.saveClient(clientId, client);
295
- } catch {
296
- return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
297
- }
298
- }
299
- if (!client) {
300
- return errorResponse(400, "invalid_client", "Unknown client_id");
427
+ const result = await resolveClient(clientId, store, config.redirectUris);
428
+ if ("status" in result) {
429
+ return result;
301
430
  }
431
+ const client = result;
302
432
  if (!client.redirectUris.includes(redirectUri)) {
303
433
  return errorResponse(400, "invalid_redirect_uri", "redirect_uri does not match registered URIs");
304
434
  }
@@ -350,42 +480,11 @@ function createOAuthProxy(config) {
350
480
  }
351
481
  await store.deletePendingAuth(proxyState);
352
482
  await store.deletePkceVerifier(proxyState);
353
- const tokenBody = new URLSearchParams({
354
- grant_type: "authorization_code",
355
- code: upstreamCode,
356
- redirect_uri: `${config.resourceUrl}${callbackPath}`,
357
- client_id: config.clientId,
358
- client_secret: config.clientSecret,
359
- code_verifier: pkceVerifier
360
- });
361
- const tokenResponse = await fetch(config.tokenUrl, {
362
- method: "POST",
363
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
364
- body: tokenBody.toString()
365
- });
366
- if (!tokenResponse.ok) {
367
- const errorBody = await tokenResponse.text();
368
- console.error("Upstream token exchange failed:", errorBody);
369
- return errorResponse(502, "upstream_error", "Failed to exchange code with upstream provider");
370
- }
371
- const tokens = await tokenResponse.json();
372
- const upstreamAccessToken = tokens.access_token;
373
- const upstreamIdToken = tokens.id_token;
374
- let email;
375
- let sub;
376
- if (config.userinfoUrl && upstreamAccessToken) {
377
- try {
378
- const userinfoResponse = await fetch(config.userinfoUrl, {
379
- headers: { Authorization: `Bearer ${upstreamAccessToken}` }
380
- });
381
- if (userinfoResponse.ok) {
382
- const userinfo = await userinfoResponse.json();
383
- email = userinfo.email;
384
- sub = userinfo.sub;
385
- }
386
- } catch {
387
- }
483
+ const upstream = await exchangeUpstreamCode(upstreamCode, config, callbackPath, pkceVerifier);
484
+ if ("status" in upstream) {
485
+ return upstream;
388
486
  }
487
+ const userinfo = await fetchUserinfo(config.userinfoUrl, upstream.accessToken);
389
488
  const mcpCode = generateCode();
390
489
  await store.saveAuthCode(mcpCode, {
391
490
  clientId: pending.clientId,
@@ -393,10 +492,10 @@ function createOAuthProxy(config) {
393
492
  codeChallenge: pending.codeChallenge,
394
493
  codeChallengeMethod: pending.codeChallengeMethod,
395
494
  scopes: pending.scope.split(" ").filter(Boolean),
396
- upstreamAccessToken,
397
- upstreamIdToken,
398
- email,
399
- sub,
495
+ upstreamAccessToken: upstream.accessToken,
496
+ upstreamIdToken: upstream.idToken,
497
+ email: userinfo.email,
498
+ sub: userinfo.sub,
400
499
  expiresAt: Date.now() / 1e3 + 600
401
500
  });
402
501
  const clientRedirect = new URL(pending.redirectUri);
@@ -411,76 +510,14 @@ function createOAuthProxy(config) {
411
510
  if (!body) {
412
511
  return errorResponse(400, "invalid_request", "Missing request body");
413
512
  }
414
- const grantType = body.grant_type;
415
- if (grantType === "refresh_token") {
416
- const refreshToken2 = body.refresh_token;
417
- if (!refreshToken2) {
418
- return errorResponse(400, "invalid_request", "Missing refresh_token");
419
- }
420
- const tokenData = await store.getRefreshToken(refreshToken2);
421
- if (!tokenData) {
422
- return errorResponse(400, "invalid_grant", "Invalid or expired refresh token");
423
- }
424
- const key2 = await getSigningKey();
425
- const now2 = Math.floor(Date.now() / 1e3);
426
- const accessToken2 = await new SignJWT({
427
- scope: tokenData.scopes.join(" "),
428
- email: tokenData.email
429
- }).setProtectedHeader({ alg: "HS256" }).setSubject(tokenData.sub ?? tokenData.email ?? tokenData.clientId).setIssuer(config.resourceUrl).setAudience(config.resourceUrl).setIssuedAt(now2).setExpirationTime(now2 + tokenTtl).sign(key2);
430
- return jsonResponse(200, {
431
- access_token: accessToken2,
432
- token_type: "Bearer",
433
- expires_in: tokenTtl,
434
- scope: tokenData.scopes.join(" "),
435
- refresh_token: refreshToken2
436
- });
513
+ const tokenConfig = { resourceUrl: config.resourceUrl, tokenTtl };
514
+ if (body.grant_type === "refresh_token") {
515
+ return handleRefreshToken(body, store, getSigningKey, tokenConfig);
437
516
  }
438
- if (grantType !== "authorization_code") {
517
+ if (body.grant_type !== "authorization_code") {
439
518
  return errorResponse(400, "unsupported_grant_type", "Supported grant types: authorization_code, refresh_token");
440
519
  }
441
- const code = body.code;
442
- const redirectUri = body.redirect_uri;
443
- const clientId = body.client_id;
444
- const codeVerifier = body.code_verifier;
445
- if (!code || !codeVerifier || !clientId) {
446
- return errorResponse(400, "invalid_request", "Missing required parameters");
447
- }
448
- const authCode = await store.getAuthCode(code);
449
- if (!authCode) {
450
- return errorResponse(400, "invalid_grant", "Invalid or expired authorization code");
451
- }
452
- await store.deleteAuthCode(code);
453
- if (authCode.clientId !== clientId) {
454
- return errorResponse(400, "invalid_grant", "client_id mismatch");
455
- }
456
- if (redirectUri && authCode.redirectUri !== redirectUri) {
457
- return errorResponse(400, "invalid_grant", "redirect_uri mismatch");
458
- }
459
- const pkceValid = await verifyPkce(codeVerifier, authCode.codeChallenge);
460
- if (!pkceValid) {
461
- return errorResponse(400, "invalid_grant", "PKCE verification failed");
462
- }
463
- const key = await getSigningKey();
464
- const now = Math.floor(Date.now() / 1e3);
465
- const accessToken = await new SignJWT({
466
- scope: authCode.scopes.join(" "),
467
- email: authCode.email
468
- }).setProtectedHeader({ alg: "HS256" }).setSubject(authCode.sub ?? authCode.email ?? authCode.clientId).setIssuer(config.resourceUrl).setAudience(config.resourceUrl).setIssuedAt(now).setExpirationTime(now + tokenTtl).sign(key);
469
- const refreshToken = randomBytes(32).toString("base64url");
470
- await store.saveRefreshToken(refreshToken, {
471
- clientId: authCode.clientId,
472
- scopes: authCode.scopes,
473
- email: authCode.email,
474
- sub: authCode.sub,
475
- expiresAt: now + 30 * 24 * 3600
476
- });
477
- return jsonResponse(200, {
478
- access_token: accessToken,
479
- token_type: "Bearer",
480
- expires_in: tokenTtl,
481
- scope: authCode.scopes.join(" "),
482
- refresh_token: refreshToken
483
- });
520
+ return handleAuthorizationCode(body, store, getSigningKey, tokenConfig);
484
521
  },
485
522
  async verifyToken(token) {
486
523
  try {
@@ -501,7 +538,6 @@ function createOAuthProxy(config) {
501
538
  }
502
539
  }
503
540
  };
504
- return provider;
505
541
  }
506
542
 
507
543
  // src/provider/google.ts
@@ -777,7 +813,6 @@ export {
777
813
  insufficientScope,
778
814
  invalidToken,
779
815
  matchRedirectUri,
780
- missingToken,
781
816
  validateToken
782
817
  };
783
818
  //# sourceMappingURL=index.js.map