@kadoa/mcp 0.3.8 → 0.3.9-rc.1

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.
Files changed (2) hide show
  1. package/dist/index.js +134 -72
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -48958,13 +48958,37 @@ function createKadoaClient(auth) {
48958
48958
  });
48959
48959
  return client;
48960
48960
  }
48961
- var ctxRefreshMutex;
48961
+ var refreshRawMutex, ctxRefreshMutex;
48962
48962
  var init_client = __esm(() => {
48963
48963
  init_dist2();
48964
+ refreshRawMutex = new Map;
48964
48965
  ctxRefreshMutex = new WeakMap;
48965
48966
  });
48966
48967
 
48967
48968
  // src/client.ts
48969
+ var exports_client = {};
48970
+ __export(exports_client, {
48971
+ refreshSupabaseJwtRaw: () => refreshSupabaseJwtRaw,
48972
+ refreshSupabaseJwt: () => refreshSupabaseJwt,
48973
+ isJwtExpired: () => isJwtExpired,
48974
+ getValidJwt: () => getValidJwt,
48975
+ decodeJwtClaims: () => decodeJwtClaims,
48976
+ createKadoaClient: () => createKadoaClient2,
48977
+ SessionExpiredError: () => SessionExpiredError,
48978
+ KadoaSdkException: () => KadoaSdkException,
48979
+ KadoaClient: () => KadoaClient
48980
+ });
48981
+ function createKadoaClient2(auth) {
48982
+ const client = new KadoaClient({ bearerToken: auth.jwt });
48983
+ client.axiosInstance.interceptors.request.use((config2) => {
48984
+ config2.headers["x-kadoa-source"] = "mcp";
48985
+ if (auth.teamId) {
48986
+ config2.headers["x-team-id"] = auth.teamId;
48987
+ }
48988
+ return config2;
48989
+ });
48990
+ return client;
48991
+ }
48968
48992
  function decodeJwtClaims(jwt2) {
48969
48993
  try {
48970
48994
  const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
@@ -48986,36 +49010,57 @@ function isJwtExpired(jwt2) {
48986
49010
  return true;
48987
49011
  }
48988
49012
  }
48989
- async function refreshSupabaseJwt(ctx) {
48990
- if (!ctx.supabaseRefreshToken) {
48991
- console.error("[JWT_REFRESH] No refresh token available, cannot refresh");
48992
- return;
49013
+ async function refreshSupabaseJwtRaw(supabaseRefreshToken) {
49014
+ const inflight = refreshRawMutex2.get(supabaseRefreshToken);
49015
+ if (inflight) {
49016
+ console.error(`[JWT_REFRESH] DEDUP: reusing in-flight raw refresh`);
49017
+ return inflight;
48993
49018
  }
49019
+ const promise3 = _doRefreshRaw(supabaseRefreshToken).finally(() => {
49020
+ refreshRawMutex2.delete(supabaseRefreshToken);
49021
+ });
49022
+ refreshRawMutex2.set(supabaseRefreshToken, promise3);
49023
+ return promise3;
49024
+ }
49025
+ async function _doRefreshRaw(supabaseRefreshToken) {
48994
49026
  const supabaseUrl = process.env.SUPABASE_URL;
48995
49027
  if (!supabaseUrl) {
48996
49028
  console.error("[JWT_REFRESH] SUPABASE_URL not set, cannot refresh");
49029
+ return null;
49030
+ }
49031
+ const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
49032
+ method: "POST",
49033
+ headers: {
49034
+ "Content-Type": "application/json",
49035
+ apikey: process.env.SUPABASE_ANON_KEY
49036
+ },
49037
+ body: JSON.stringify({ refresh_token: supabaseRefreshToken })
49038
+ });
49039
+ if (res.ok) {
49040
+ const data = await res.json();
49041
+ return { jwt: data.access_token, refreshToken: data.refresh_token };
49042
+ }
49043
+ const body = await res.text().catch(() => "");
49044
+ console.error(`[JWT_REFRESH] FAIL: Supabase returned ${res.status} (refreshToken=${supabaseRefreshToken.slice(0, 12)}...): ${body}`);
49045
+ if (body.includes("session_expired") || body.includes("refresh_token_not_found") || body.includes("refresh_token_already_used")) {
49046
+ throw new SessionExpiredError("Your Kadoa session has expired due to inactivity. Please reconnect to re-authenticate.");
49047
+ }
49048
+ return null;
49049
+ }
49050
+ async function refreshSupabaseJwt(ctx) {
49051
+ if (!ctx.supabaseRefreshToken) {
49052
+ console.error("[JWT_REFRESH] No refresh token available, cannot refresh");
48997
49053
  return;
48998
49054
  }
48999
49055
  try {
49000
49056
  const refreshToken = ctx.supabaseRefreshToken;
49001
49057
  console.error(`[JWT_REFRESH] Refreshing Supabase JWT (refreshToken=${refreshToken.slice(0, 12)}..., team=${ctx.teamId ?? "unknown"})`);
49002
- const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
49003
- method: "POST",
49004
- headers: {
49005
- "Content-Type": "application/json",
49006
- apikey: process.env.SUPABASE_ANON_KEY
49007
- },
49008
- body: JSON.stringify({ refresh_token: refreshToken })
49009
- });
49010
- if (!res.ok) {
49011
- const body = await res.text().catch(() => "");
49012
- console.error(`[JWT_REFRESH] FAIL: Supabase returned ${res.status} (refreshToken=${refreshToken.slice(0, 12)}...): ${body}`);
49058
+ const result = await refreshSupabaseJwtRaw(refreshToken);
49059
+ if (!result)
49013
49060
  return;
49014
- }
49015
- const data = await res.json();
49016
- ctx.supabaseJwt = data.access_token;
49017
- ctx.supabaseRefreshToken = data.refresh_token;
49018
- ctx.client.setBearerToken(data.access_token);
49061
+ ctx.supabaseJwt = result.jwt;
49062
+ ctx.supabaseRefreshToken = result.refreshToken;
49063
+ ctx.client.setBearerToken(result.jwt);
49019
49064
  try {
49020
49065
  await ctx.persist?.({
49021
49066
  supabaseJwt: ctx.supabaseJwt,
@@ -49025,9 +49070,11 @@ async function refreshSupabaseJwt(ctx) {
49025
49070
  } catch (e) {
49026
49071
  console.error("[JWT_REFRESH] WARN: persist failed, tokens updated in-memory only:", e);
49027
49072
  }
49028
- console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"}, newRefreshToken=${data.refresh_token.slice(0, 12)}...)`);
49029
- return data.access_token;
49073
+ console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"}, newRefreshToken=${result.refreshToken.slice(0, 12)}...)`);
49074
+ return result.jwt;
49030
49075
  } catch (error48) {
49076
+ if (error48 instanceof SessionExpiredError)
49077
+ throw error48;
49031
49078
  console.error("[JWT_REFRESH] FAIL: threw", error48);
49032
49079
  return;
49033
49080
  }
@@ -49048,9 +49095,16 @@ async function getValidJwt(ctx) {
49048
49095
  ctxRefreshMutex2.set(ctx, promise3);
49049
49096
  return promise3;
49050
49097
  }
49051
- var ctxRefreshMutex2;
49098
+ var SessionExpiredError, refreshRawMutex2, ctxRefreshMutex2;
49052
49099
  var init_client2 = __esm(() => {
49053
49100
  init_dist2();
49101
+ SessionExpiredError = class SessionExpiredError extends Error {
49102
+ constructor(message) {
49103
+ super(message ?? "Supabase session expired. Please re-authenticate.");
49104
+ this.name = "SessionExpiredError";
49105
+ }
49106
+ };
49107
+ refreshRawMutex2 = new Map;
49054
49108
  ctxRefreshMutex2 = new WeakMap;
49055
49109
  });
49056
49110
 
@@ -49168,6 +49222,10 @@ function registerTools(server, ctx) {
49168
49222
  }
49169
49223
  return await handler(...args);
49170
49224
  } catch (error48) {
49225
+ if (error48 instanceof SessionExpiredError) {
49226
+ console.error(`[Tool Error] ${name}: session expired, user must re-authenticate`);
49227
+ return errorResult("Your session has expired. Please reconnect the MCP server to re-authenticate.");
49228
+ }
49171
49229
  let message = classifyError(error48);
49172
49230
  if (KadoaHttpException.isInstance(error48) && error48.httpStatus === 403) {
49173
49231
  try {
@@ -53892,45 +53950,6 @@ function generatePKCE() {
53892
53950
  const challenge = createHash2("sha256").update(verifier).digest("base64url");
53893
53951
  return { verifier, challenge };
53894
53952
  }
53895
- async function refreshSupabaseToken(supabaseRefreshToken, context) {
53896
- const inflight = supabaseRefreshMutex.get(supabaseRefreshToken);
53897
- if (inflight) {
53898
- console.error(`[AUTH] REFRESH_DEDUP: reusing in-flight refresh (${context})`);
53899
- return inflight;
53900
- }
53901
- const promise3 = (async () => {
53902
- const supabaseUrl = process.env.SUPABASE_URL;
53903
- if (!supabaseUrl || !supabaseRefreshToken)
53904
- return null;
53905
- try {
53906
- const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
53907
- method: "POST",
53908
- headers: {
53909
- "Content-Type": "application/json",
53910
- apikey: process.env.SUPABASE_ANON_KEY
53911
- },
53912
- body: JSON.stringify({ refresh_token: supabaseRefreshToken })
53913
- });
53914
- if (res.ok) {
53915
- const data = await res.json();
53916
- const newClaims = jwtClaims(data.access_token);
53917
- console.log(`[AUTH] REFRESH_OK: Supabase JWT refreshed (${context}, newEmail=${newClaims.email})`);
53918
- return { jwt: data.access_token, refreshToken: data.refresh_token };
53919
- }
53920
- const body = await res.text().catch(() => "");
53921
- console.error(`[AUTH] REFRESH_FAIL: Supabase returned ${res.status} (${context}): ${body.slice(0, 200)}`);
53922
- return null;
53923
- } catch (err) {
53924
- console.error(`[AUTH] REFRESH_FAIL: Supabase refresh threw (${context}):`, err);
53925
- return null;
53926
- }
53927
- })();
53928
- supabaseRefreshMutex.set(supabaseRefreshToken, promise3);
53929
- promise3.finally(() => {
53930
- supabaseRefreshMutex.delete(supabaseRefreshToken);
53931
- });
53932
- return promise3;
53933
- }
53934
53953
  function jwtClaims(jwt2) {
53935
53954
  try {
53936
53955
  const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
@@ -54238,12 +54257,22 @@ class KadoaOAuthProvider {
54238
54257
  let { supabaseJwt, supabaseRefreshToken } = entry;
54239
54258
  const claims = jwtClaims(entry.supabaseJwt);
54240
54259
  const context = `email=${claims.email}, team=${entry.teamId}`;
54241
- const refreshed = await refreshSupabaseToken(supabaseRefreshToken, context);
54242
- if (refreshed) {
54243
- supabaseJwt = refreshed.jwt;
54244
- supabaseRefreshToken = refreshed.refreshToken;
54245
- } else {
54246
- console.error(`[AUTH] REFRESH_WARN: using stale Supabase JWT as fallback (${context})`);
54260
+ try {
54261
+ const { refreshSupabaseJwtRaw: refreshSupabaseJwtRaw2, SessionExpiredError: SessionExpiredError2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
54262
+ const refreshed = await refreshSupabaseJwtRaw2(supabaseRefreshToken);
54263
+ if (refreshed) {
54264
+ supabaseJwt = refreshed.jwt;
54265
+ supabaseRefreshToken = refreshed.refreshToken;
54266
+ console.error(`[AUTH] REFRESH_OK: Supabase JWT refreshed (${context})`);
54267
+ } else {
54268
+ console.error(`[AUTH] REFRESH_WARN: using stale Supabase JWT as fallback (${context})`);
54269
+ }
54270
+ } catch (error48) {
54271
+ if (error48 instanceof Error && error48.name === "SessionExpiredError") {
54272
+ console.error(`[AUTH] REFRESH_DEAD: session permanently expired (${context}): ${error48.message}`);
54273
+ throw new InvalidTokenError("Supabase session expired. Please re-authenticate.");
54274
+ }
54275
+ console.error(`[AUTH] REFRESH_WARN: unexpected error, using stale JWT (${context}):`, error48);
54247
54276
  }
54248
54277
  const freshClaims = jwtClaims(supabaseJwt);
54249
54278
  const teamId = freshClaims.activeTeamId ?? entry.teamId;
@@ -54370,7 +54399,7 @@ class KadoaOAuthProvider {
54370
54399
  codeChallenge: pending.params.codeChallenge,
54371
54400
  clientId: pending.client.client_id,
54372
54401
  redirectUri: pending.params.redirectUri,
54373
- expiresAt: Date.now() + 10 * 60 * 1000
54402
+ expiresAt: Date.now() + 600000
54374
54403
  }, 600);
54375
54404
  const redirectUrl = new URL(pending.params.redirectUri);
54376
54405
  redirectUrl.searchParams.set("code", mcpCode);
@@ -54799,12 +54828,11 @@ function renderLoginPage(state, error48) {
54799
54828
  function escapeHtml(str) {
54800
54829
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
54801
54830
  }
54802
- var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL, supabaseRefreshMutex;
54831
+ var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL;
54803
54832
  var init_auth2 = __esm(() => {
54804
54833
  init_errors4();
54805
54834
  TEAM_SELECTION_TTL = 10 * 60 * 1000;
54806
54835
  ACCESS_TOKEN_TTL = 7 * 24 * 3600;
54807
- supabaseRefreshMutex = new Map;
54808
54836
  });
54809
54837
 
54810
54838
  // src/redis-store.ts
@@ -54997,6 +55025,39 @@ async function startHttpServer(options) {
54997
55025
  return;
54998
55026
  }
54999
55027
  const identity = `jwt:${auth.userId.slice(0, 8)}...:team=${auth.teamId.slice(0, 8)}...`;
55028
+ if (isJwtExpired(auth.jwt)) {
55029
+ try {
55030
+ const refreshed = await refreshSupabaseJwtRaw(auth.refreshToken);
55031
+ if (refreshed) {
55032
+ auth.jwt = refreshed.jwt;
55033
+ auth.refreshToken = refreshed.refreshToken;
55034
+ const entry = await store.get("access_tokens", auth.mcpToken);
55035
+ if (entry) {
55036
+ const remainingMs = entry.expiresAt - Date.now();
55037
+ if (remainingMs > 0) {
55038
+ await store.set("access_tokens", auth.mcpToken, {
55039
+ ...entry,
55040
+ supabaseJwt: refreshed.jwt,
55041
+ supabaseRefreshToken: refreshed.refreshToken
55042
+ }, Math.ceil(remainingMs / 1000));
55043
+ console.error(`[PROACTIVE_REFRESH] OK: JWT refreshed on ${method} (${identity})`);
55044
+ }
55045
+ }
55046
+ }
55047
+ } catch (error48) {
55048
+ if (error48 instanceof SessionExpiredError) {
55049
+ console.error(`[PROACTIVE_REFRESH] Session dead on ${method} (${identity}): ${error48.message}`);
55050
+ await store.del("access_tokens", auth.mcpToken);
55051
+ res.status(401).json({
55052
+ jsonrpc: "2.0",
55053
+ error: { code: -32001, message: error48.message },
55054
+ id: req.body?.id ?? null
55055
+ });
55056
+ return;
55057
+ }
55058
+ console.error(`[PROACTIVE_REFRESH] WARN: refresh failed on ${method}, continuing with stale JWT`, error48);
55059
+ }
55060
+ }
55000
55061
  try {
55001
55062
  console.error(`[MCP] POST method=${method} auth=${identity}`);
55002
55063
  const transport = new StreamableHTTPServerTransport({
@@ -55074,6 +55135,7 @@ var init_http2 = __esm(async () => {
55074
55135
  init_bearerAuth();
55075
55136
  init_auth2();
55076
55137
  init_redis_store();
55138
+ init_client2();
55077
55139
  await init_src();
55078
55140
  });
55079
55141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kadoa/mcp",
3
- "version": "0.3.8",
3
+ "version": "0.3.9-rc.1",
4
4
  "description": "Kadoa MCP Server — manage workflows from Claude Desktop, Cursor, and other MCP clients",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",