@kadoa/mcp 0.5.10 → 0.5.11
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/dist/index.js +309 -94
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -51861,13 +51861,23 @@ This is the main page content.`
|
|
|
51861
51861
|
});
|
|
51862
51862
|
|
|
51863
51863
|
// src/client.ts
|
|
51864
|
+
function resolveServiceAccountApiKey(credentialRef) {
|
|
51865
|
+
const configuredRef = process.env.MCP_SERVICE_ACCOUNT_CREDENTIAL_REF?.trim();
|
|
51866
|
+
if (!configuredRef || credentialRef !== configuredRef) {
|
|
51867
|
+
throw new Error("Unknown service-account credential reference");
|
|
51868
|
+
}
|
|
51869
|
+
const apiKey = process.env.MCP_SERVICE_ACCOUNT_KADOA_API_KEY?.trim();
|
|
51870
|
+
if (!apiKey) {
|
|
51871
|
+
throw new Error("Service-account API key is not configured");
|
|
51872
|
+
}
|
|
51873
|
+
return apiKey;
|
|
51874
|
+
}
|
|
51864
51875
|
function createKadoaClient(auth) {
|
|
51865
|
-
const
|
|
51876
|
+
const { principal } = auth;
|
|
51877
|
+
const client = principal.type === "user" ? new KadoaClient({ bearerToken: principal.supabaseJwt }) : new KadoaClient({ apiKey: auth.apiKey ?? resolveServiceAccountApiKey(principal.credentialRef) });
|
|
51866
51878
|
client.axiosInstance.interceptors.request.use((config2) => {
|
|
51867
51879
|
config2.headers["x-kadoa-source"] = "mcp";
|
|
51868
|
-
|
|
51869
|
-
config2.headers["x-team-id"] = auth.teamId;
|
|
51870
|
-
}
|
|
51880
|
+
config2.headers["x-team-id"] = principal.teamId;
|
|
51871
51881
|
return config2;
|
|
51872
51882
|
});
|
|
51873
51883
|
return client;
|
|
@@ -51882,6 +51892,7 @@ var init_client = __esm(() => {
|
|
|
51882
51892
|
// src/client.ts
|
|
51883
51893
|
var exports_client = {};
|
|
51884
51894
|
__export(exports_client, {
|
|
51895
|
+
resolveServiceAccountApiKey: () => resolveServiceAccountApiKey2,
|
|
51885
51896
|
refreshSupabaseJwtRaw: () => refreshSupabaseJwtRaw,
|
|
51886
51897
|
refreshSupabaseJwt: () => refreshSupabaseJwt,
|
|
51887
51898
|
isJwtExpired: () => isJwtExpired,
|
|
@@ -51892,13 +51903,23 @@ __export(exports_client, {
|
|
|
51892
51903
|
KadoaSdkException: () => KadoaSdkException,
|
|
51893
51904
|
KadoaClient: () => KadoaClient
|
|
51894
51905
|
});
|
|
51906
|
+
function resolveServiceAccountApiKey2(credentialRef) {
|
|
51907
|
+
const configuredRef = process.env.MCP_SERVICE_ACCOUNT_CREDENTIAL_REF?.trim();
|
|
51908
|
+
if (!configuredRef || credentialRef !== configuredRef) {
|
|
51909
|
+
throw new Error("Unknown service-account credential reference");
|
|
51910
|
+
}
|
|
51911
|
+
const apiKey = process.env.MCP_SERVICE_ACCOUNT_KADOA_API_KEY?.trim();
|
|
51912
|
+
if (!apiKey) {
|
|
51913
|
+
throw new Error("Service-account API key is not configured");
|
|
51914
|
+
}
|
|
51915
|
+
return apiKey;
|
|
51916
|
+
}
|
|
51895
51917
|
function createKadoaClient2(auth) {
|
|
51896
|
-
const
|
|
51918
|
+
const { principal } = auth;
|
|
51919
|
+
const client = principal.type === "user" ? new KadoaClient({ bearerToken: principal.supabaseJwt }) : new KadoaClient({ apiKey: auth.apiKey ?? resolveServiceAccountApiKey2(principal.credentialRef) });
|
|
51897
51920
|
client.axiosInstance.interceptors.request.use((config2) => {
|
|
51898
51921
|
config2.headers["x-kadoa-source"] = "mcp";
|
|
51899
|
-
|
|
51900
|
-
config2.headers["x-team-id"] = auth.teamId;
|
|
51901
|
-
}
|
|
51922
|
+
config2.headers["x-team-id"] = principal.teamId;
|
|
51902
51923
|
return config2;
|
|
51903
51924
|
});
|
|
51904
51925
|
return client;
|
|
@@ -51955,7 +51976,7 @@ async function _doRefreshRaw(supabaseRefreshToken) {
|
|
|
51955
51976
|
return { jwt: data.access_token, refreshToken: data.refresh_token };
|
|
51956
51977
|
}
|
|
51957
51978
|
const body = await res.text().catch(() => "");
|
|
51958
|
-
console.error(`[JWT_REFRESH] FAIL: Supabase returned ${res.status}
|
|
51979
|
+
console.error(`[JWT_REFRESH] FAIL: Supabase returned ${res.status}: ${body}`);
|
|
51959
51980
|
if (body.includes("session_expired") || body.includes("refresh_token_not_found") || body.includes("refresh_token_already_used")) {
|
|
51960
51981
|
throw new SessionExpiredError("Your Kadoa session has expired due to inactivity. Please reconnect to re-authenticate.");
|
|
51961
51982
|
}
|
|
@@ -51968,7 +51989,7 @@ async function refreshSupabaseJwt(ctx) {
|
|
|
51968
51989
|
}
|
|
51969
51990
|
try {
|
|
51970
51991
|
const refreshToken = ctx.supabaseRefreshToken;
|
|
51971
|
-
console.error(`[JWT_REFRESH] Refreshing Supabase JWT (
|
|
51992
|
+
console.error(`[JWT_REFRESH] Refreshing Supabase JWT (team=${ctx.teamId ?? "unknown"})`);
|
|
51972
51993
|
const result = await refreshSupabaseJwtRaw(refreshToken);
|
|
51973
51994
|
if (!result)
|
|
51974
51995
|
return;
|
|
@@ -51984,7 +52005,7 @@ async function refreshSupabaseJwt(ctx) {
|
|
|
51984
52005
|
} catch (e) {
|
|
51985
52006
|
console.error("[JWT_REFRESH] WARN: persist failed, tokens updated in-memory only:", e);
|
|
51986
52007
|
}
|
|
51987
|
-
console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"}
|
|
52008
|
+
console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"})`);
|
|
51988
52009
|
return result.jwt;
|
|
51989
52010
|
} catch (error48) {
|
|
51990
52011
|
if (error48 instanceof SessionExpiredError)
|
|
@@ -52307,6 +52328,12 @@ function registerTools(server, ctx, capabilities) {
|
|
|
52307
52328
|
inputSchema: {},
|
|
52308
52329
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
|
|
52309
52330
|
}, withErrorHandling("whoami", async () => {
|
|
52331
|
+
if (ctx.principal.type === "serviceAccount") {
|
|
52332
|
+
return jsonResult({
|
|
52333
|
+
authMethod: "Shared organization service account",
|
|
52334
|
+
teamId: ctx.principal.teamId
|
|
52335
|
+
});
|
|
52336
|
+
}
|
|
52310
52337
|
const jwt2 = await getValidJwt(ctx);
|
|
52311
52338
|
const user = await ctx.client.user.getCurrentUser();
|
|
52312
52339
|
const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
|
|
@@ -53181,6 +53208,12 @@ function registerTools(server, ctx, capabilities) {
|
|
|
53181
53208
|
inputSchema: {},
|
|
53182
53209
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
|
|
53183
53210
|
}, withErrorHandling("team_list", async () => {
|
|
53211
|
+
if (ctx.principal.type === "serviceAccount") {
|
|
53212
|
+
return jsonResult({
|
|
53213
|
+
teams: [{ id: ctx.principal.teamId, active: true, sharedServiceAccount: true }],
|
|
53214
|
+
activeTeamId: ctx.principal.teamId
|
|
53215
|
+
});
|
|
53216
|
+
}
|
|
53184
53217
|
const jwt2 = await getValidJwt(ctx);
|
|
53185
53218
|
const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
|
|
53186
53219
|
const activeTeamId = ctx.teamId ?? teams[0]?.id;
|
|
@@ -53202,6 +53235,9 @@ function registerTools(server, ctx, capabilities) {
|
|
|
53202
53235
|
},
|
|
53203
53236
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
53204
53237
|
}, withErrorHandling("team_switch", async (args) => {
|
|
53238
|
+
if (ctx.principal.type === "serviceAccount") {
|
|
53239
|
+
return errorResult("This shared organization connector is fixed to one team and cannot switch teams.");
|
|
53240
|
+
}
|
|
53205
53241
|
const jwt2 = await getValidJwt(ctx);
|
|
53206
53242
|
const teams = await ctx.client.listTeams({ bearerToken: jwt2 });
|
|
53207
53243
|
const identifier = args.teamIdentifier;
|
|
@@ -53619,7 +53655,7 @@ var package_default;
|
|
|
53619
53655
|
var init_package = __esm(() => {
|
|
53620
53656
|
package_default = {
|
|
53621
53657
|
name: "@kadoa/mcp",
|
|
53622
|
-
version: "0.5.
|
|
53658
|
+
version: "0.5.11",
|
|
53623
53659
|
description: "Kadoa MCP Server — manage workflows from Claude Desktop, Cursor, and other MCP clients",
|
|
53624
53660
|
type: "module",
|
|
53625
53661
|
main: "dist/index.js",
|
|
@@ -57651,11 +57687,87 @@ var init_bearerAuth = __esm(() => {
|
|
|
57651
57687
|
init_errors4();
|
|
57652
57688
|
});
|
|
57653
57689
|
|
|
57690
|
+
// src/service-account-config.ts
|
|
57691
|
+
function getServiceAccountOAuthConfig() {
|
|
57692
|
+
const values = Object.fromEntries(Object.entries(ENV_KEYS).map(([field, envKey]) => [field, process.env[envKey]?.trim()]));
|
|
57693
|
+
if (Object.values(values).every((value) => !value))
|
|
57694
|
+
return;
|
|
57695
|
+
const missing = Object.entries(values).filter(([, value]) => !value).map(([field]) => ENV_KEYS[field]);
|
|
57696
|
+
if (missing.length > 0) {
|
|
57697
|
+
throw new Error(`Incomplete service-account OAuth configuration; missing ${missing.join(", ")}`);
|
|
57698
|
+
}
|
|
57699
|
+
const redirectUri = new URL(values.redirectUri);
|
|
57700
|
+
if (redirectUri.protocol !== "https:" && redirectUri.hostname !== "localhost") {
|
|
57701
|
+
throw new Error("MCP_SERVICE_ACCOUNT_OAUTH_REDIRECT_URI must use HTTPS");
|
|
57702
|
+
}
|
|
57703
|
+
return {
|
|
57704
|
+
clientId: values.clientId,
|
|
57705
|
+
clientSecret: values.clientSecret,
|
|
57706
|
+
redirectUri: redirectUri.toString(),
|
|
57707
|
+
credentialRef: values.credentialRef,
|
|
57708
|
+
teamId: values.teamId,
|
|
57709
|
+
label: values.label
|
|
57710
|
+
};
|
|
57711
|
+
}
|
|
57712
|
+
function getConfiguredServiceAccountClient(clientId) {
|
|
57713
|
+
const configuredClientId = process.env.MCP_SERVICE_ACCOUNT_OAUTH_CLIENT_ID?.trim();
|
|
57714
|
+
if (!configuredClientId || configuredClientId !== clientId)
|
|
57715
|
+
return;
|
|
57716
|
+
const config2 = getServiceAccountOAuthConfig();
|
|
57717
|
+
if (!config2)
|
|
57718
|
+
return;
|
|
57719
|
+
return {
|
|
57720
|
+
client_id: config2.clientId,
|
|
57721
|
+
client_secret: config2.clientSecret,
|
|
57722
|
+
client_id_issued_at: 0,
|
|
57723
|
+
client_name: config2.label,
|
|
57724
|
+
redirect_uris: [config2.redirectUri],
|
|
57725
|
+
token_endpoint_auth_method: "client_secret_post",
|
|
57726
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
57727
|
+
response_types: ["code"]
|
|
57728
|
+
};
|
|
57729
|
+
}
|
|
57730
|
+
var ENV_KEYS;
|
|
57731
|
+
var init_service_account_config = __esm(() => {
|
|
57732
|
+
ENV_KEYS = {
|
|
57733
|
+
clientId: "MCP_SERVICE_ACCOUNT_OAUTH_CLIENT_ID",
|
|
57734
|
+
clientSecret: "MCP_SERVICE_ACCOUNT_OAUTH_CLIENT_SECRET",
|
|
57735
|
+
redirectUri: "MCP_SERVICE_ACCOUNT_OAUTH_REDIRECT_URI",
|
|
57736
|
+
credentialRef: "MCP_SERVICE_ACCOUNT_CREDENTIAL_REF",
|
|
57737
|
+
teamId: "MCP_SERVICE_ACCOUNT_TEAM_ID",
|
|
57738
|
+
label: "MCP_SERVICE_ACCOUNT_LABEL"
|
|
57739
|
+
};
|
|
57740
|
+
});
|
|
57741
|
+
|
|
57654
57742
|
// src/auth.ts
|
|
57655
57743
|
import { randomBytes, createHash as createHash2 } from "node:crypto";
|
|
57656
57744
|
function randomToken(bytes = 32) {
|
|
57657
57745
|
return randomBytes(bytes).toString("hex");
|
|
57658
57746
|
}
|
|
57747
|
+
function readPrincipal(entry) {
|
|
57748
|
+
if (entry.principal)
|
|
57749
|
+
return entry.principal;
|
|
57750
|
+
if (entry.supabaseJwt && entry.supabaseRefreshToken) {
|
|
57751
|
+
return {
|
|
57752
|
+
type: "user",
|
|
57753
|
+
supabaseJwt: entry.supabaseJwt,
|
|
57754
|
+
supabaseRefreshToken: entry.supabaseRefreshToken,
|
|
57755
|
+
teamId: entry.teamId
|
|
57756
|
+
};
|
|
57757
|
+
}
|
|
57758
|
+
throw new InvalidTokenError("Stored token has no valid principal");
|
|
57759
|
+
}
|
|
57760
|
+
function persistPrincipal(principal) {
|
|
57761
|
+
if (principal.type === "serviceAccount") {
|
|
57762
|
+
return { principal, teamId: principal.teamId };
|
|
57763
|
+
}
|
|
57764
|
+
return {
|
|
57765
|
+
principal,
|
|
57766
|
+
supabaseJwt: principal.supabaseJwt,
|
|
57767
|
+
supabaseRefreshToken: principal.supabaseRefreshToken,
|
|
57768
|
+
teamId: principal.teamId
|
|
57769
|
+
};
|
|
57770
|
+
}
|
|
57659
57771
|
function generatePKCE() {
|
|
57660
57772
|
const verifier = randomBytes(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "").slice(0, 128);
|
|
57661
57773
|
const challenge = createHash2("sha256").update(verifier).digest("base64url");
|
|
@@ -57753,7 +57865,7 @@ class KadoaOAuthProvider {
|
|
|
57753
57865
|
const store = this.store;
|
|
57754
57866
|
return {
|
|
57755
57867
|
async getClient(clientId) {
|
|
57756
|
-
return store.get("clients", clientId);
|
|
57868
|
+
return getConfiguredServiceAccountClient(clientId) ?? store.get("clients", clientId);
|
|
57757
57869
|
},
|
|
57758
57870
|
async registerClient(client) {
|
|
57759
57871
|
const clientId = randomToken(16);
|
|
@@ -57768,10 +57880,9 @@ class KadoaOAuthProvider {
|
|
|
57768
57880
|
};
|
|
57769
57881
|
}
|
|
57770
57882
|
async authorize(client, params, res) {
|
|
57771
|
-
const supabaseUrl = process.env.SUPABASE_URL;
|
|
57772
57883
|
const serverUrl = process.env.MCP_SERVER_URL;
|
|
57773
|
-
if (!
|
|
57774
|
-
throw new Error("
|
|
57884
|
+
if (!serverUrl) {
|
|
57885
|
+
throw new Error("MCP_SERVER_URL must be configured");
|
|
57775
57886
|
}
|
|
57776
57887
|
const state = randomToken();
|
|
57777
57888
|
const { verifier, challenge } = generatePKCE();
|
|
@@ -57780,6 +57891,14 @@ class KadoaOAuthProvider {
|
|
|
57780
57891
|
params,
|
|
57781
57892
|
supabaseCodeVerifier: verifier
|
|
57782
57893
|
}, 600);
|
|
57894
|
+
const configuredServiceClientId = process.env.MCP_SERVICE_ACCOUNT_OAUTH_CLIENT_ID?.trim();
|
|
57895
|
+
if (configuredServiceClientId === client.client_id) {
|
|
57896
|
+
const serviceConfig = getServiceAccountOAuthConfig();
|
|
57897
|
+
if (!serviceConfig)
|
|
57898
|
+
throw new Error("Service-account OAuth configuration is unavailable");
|
|
57899
|
+
res.type("html").send(renderServiceAccountConsentPage(state, serviceConfig.label));
|
|
57900
|
+
return;
|
|
57901
|
+
}
|
|
57783
57902
|
res.type("html").send(renderLoginPage(state));
|
|
57784
57903
|
}
|
|
57785
57904
|
async handleGoogleLogin(req, res) {
|
|
@@ -57897,8 +58016,9 @@ class KadoaOAuthProvider {
|
|
|
57897
58016
|
if (teams.length === 1) {
|
|
57898
58017
|
const refreshed = await setActiveTeamAndRefresh(supabaseJwt, supabaseRefreshToken, teams[0].id);
|
|
57899
58018
|
await this.completeAuthFlow(pending, res, {
|
|
57900
|
-
|
|
57901
|
-
|
|
58019
|
+
type: "user",
|
|
58020
|
+
supabaseJwt: refreshed.jwt,
|
|
58021
|
+
supabaseRefreshToken: refreshed.refreshToken,
|
|
57902
58022
|
teamId: teams[0].id
|
|
57903
58023
|
});
|
|
57904
58024
|
return;
|
|
@@ -57927,29 +58047,28 @@ class KadoaOAuthProvider {
|
|
|
57927
58047
|
await this.store.del("auth_codes", authorizationCode);
|
|
57928
58048
|
throw new Error("Authorization code expired");
|
|
57929
58049
|
}
|
|
58050
|
+
if (entry.clientId !== _client.client_id) {
|
|
58051
|
+
throw new Error("OAuth client mismatch for authorization code");
|
|
58052
|
+
}
|
|
57930
58053
|
if (redirectUri && redirectUri !== entry.redirectUri) {
|
|
57931
58054
|
throw new Error("redirect_uri mismatch");
|
|
57932
58055
|
}
|
|
57933
58056
|
const accessToken = randomToken();
|
|
57934
58057
|
const refreshToken = randomToken();
|
|
57935
58058
|
const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
|
|
58059
|
+
const principal = readPrincipal(entry);
|
|
57936
58060
|
await this.store.set("access_tokens", accessToken, {
|
|
57937
|
-
|
|
57938
|
-
supabaseRefreshToken: entry.supabaseRefreshToken,
|
|
57939
|
-
teamId: entry.teamId,
|
|
58061
|
+
...persistPrincipal(principal),
|
|
57940
58062
|
clientId: entry.clientId,
|
|
57941
58063
|
expiresAt
|
|
57942
58064
|
}, ACCESS_TOKEN_TTL);
|
|
57943
58065
|
await this.store.set("refresh_tokens", refreshToken, {
|
|
57944
|
-
|
|
57945
|
-
supabaseRefreshToken: entry.supabaseRefreshToken,
|
|
57946
|
-
teamId: entry.teamId,
|
|
58066
|
+
...persistPrincipal(principal),
|
|
57947
58067
|
clientId: entry.clientId
|
|
57948
58068
|
}, 2592000);
|
|
57949
58069
|
await this.store.del("auth_codes", authorizationCode);
|
|
57950
|
-
const claims = jwtClaims(entry.supabaseJwt);
|
|
57951
58070
|
const sessionCount = await this.store.size("access_tokens");
|
|
57952
|
-
console.log(`[AUTH] LOGIN: tokens issued (
|
|
58071
|
+
console.log(`[AUTH] LOGIN: tokens issued (mode=${principal.type}, team=${principal.teamId}, ttl=${ACCESS_TOKEN_TTL}s, active_sessions=${sessionCount})`);
|
|
57953
58072
|
return {
|
|
57954
58073
|
access_token: accessToken,
|
|
57955
58074
|
token_type: "bearer",
|
|
@@ -57961,12 +58080,36 @@ class KadoaOAuthProvider {
|
|
|
57961
58080
|
const entry = await this.store.get("refresh_tokens", refreshToken);
|
|
57962
58081
|
if (!entry) {
|
|
57963
58082
|
const sessionCount = await this.store.size("refresh_tokens");
|
|
57964
|
-
console.error(`[AUTH] REFRESH_FAIL: unknown refresh token (
|
|
58083
|
+
console.error(`[AUTH] REFRESH_FAIL: unknown refresh token (active_sessions=${sessionCount})`);
|
|
57965
58084
|
throw new InvalidTokenError("Unknown or expired refresh token");
|
|
57966
58085
|
}
|
|
58086
|
+
if (entry.clientId !== _client.client_id) {
|
|
58087
|
+
throw new Error("OAuth client mismatch for refresh token");
|
|
58088
|
+
}
|
|
57967
58089
|
await this.store.del("refresh_tokens", refreshToken);
|
|
57968
|
-
|
|
57969
|
-
|
|
58090
|
+
const storedPrincipal = readPrincipal(entry);
|
|
58091
|
+
if (storedPrincipal.type === "serviceAccount") {
|
|
58092
|
+
const newAccessToken2 = randomToken();
|
|
58093
|
+
const newRefreshToken2 = randomToken();
|
|
58094
|
+
const expiresAt2 = Date.now() + ACCESS_TOKEN_TTL * 1000;
|
|
58095
|
+
await this.store.set("access_tokens", newAccessToken2, {
|
|
58096
|
+
...persistPrincipal(storedPrincipal),
|
|
58097
|
+
clientId: entry.clientId,
|
|
58098
|
+
expiresAt: expiresAt2
|
|
58099
|
+
}, ACCESS_TOKEN_TTL);
|
|
58100
|
+
await this.store.set("refresh_tokens", newRefreshToken2, {
|
|
58101
|
+
...persistPrincipal(storedPrincipal),
|
|
58102
|
+
clientId: entry.clientId
|
|
58103
|
+
}, 2592000);
|
|
58104
|
+
return {
|
|
58105
|
+
access_token: newAccessToken2,
|
|
58106
|
+
token_type: "bearer",
|
|
58107
|
+
expires_in: ACCESS_TOKEN_TTL,
|
|
58108
|
+
refresh_token: newRefreshToken2
|
|
58109
|
+
};
|
|
58110
|
+
}
|
|
58111
|
+
let { supabaseJwt, supabaseRefreshToken } = storedPrincipal;
|
|
58112
|
+
const claims = jwtClaims(storedPrincipal.supabaseJwt);
|
|
57970
58113
|
const context = `email=${claims.email}, team=${entry.teamId}`;
|
|
57971
58114
|
try {
|
|
57972
58115
|
const { refreshSupabaseJwtRaw: refreshSupabaseJwtRaw2, SessionExpiredError: SessionExpiredError2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
|
|
@@ -57990,17 +58133,19 @@ class KadoaOAuthProvider {
|
|
|
57990
58133
|
const newAccessToken = randomToken();
|
|
57991
58134
|
const newRefreshToken = randomToken();
|
|
57992
58135
|
const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
|
|
57993
|
-
|
|
58136
|
+
const refreshedPrincipal = {
|
|
58137
|
+
type: "user",
|
|
57994
58138
|
supabaseJwt,
|
|
57995
58139
|
supabaseRefreshToken,
|
|
57996
|
-
teamId
|
|
58140
|
+
teamId
|
|
58141
|
+
};
|
|
58142
|
+
await this.store.set("access_tokens", newAccessToken, {
|
|
58143
|
+
...persistPrincipal(refreshedPrincipal),
|
|
57997
58144
|
clientId: entry.clientId,
|
|
57998
58145
|
expiresAt
|
|
57999
58146
|
}, ACCESS_TOKEN_TTL);
|
|
58000
58147
|
await this.store.set("refresh_tokens", newRefreshToken, {
|
|
58001
|
-
|
|
58002
|
-
supabaseRefreshToken,
|
|
58003
|
-
teamId,
|
|
58148
|
+
...persistPrincipal(refreshedPrincipal),
|
|
58004
58149
|
clientId: entry.clientId
|
|
58005
58150
|
}, 2592000);
|
|
58006
58151
|
return {
|
|
@@ -58014,25 +58159,28 @@ class KadoaOAuthProvider {
|
|
|
58014
58159
|
const entry = await this.store.get("access_tokens", token);
|
|
58015
58160
|
if (!entry) {
|
|
58016
58161
|
const sessionCount = await this.store.size("access_tokens");
|
|
58017
|
-
console.error(`[AUTH] VERIFY_FAIL: unknown token (
|
|
58162
|
+
console.error(`[AUTH] VERIFY_FAIL: unknown token (active_sessions=${sessionCount})`);
|
|
58018
58163
|
throw new InvalidTokenError("Unknown or expired access token");
|
|
58019
58164
|
}
|
|
58020
58165
|
if (entry.expiresAt < Date.now()) {
|
|
58021
58166
|
const expiredAgo = Math.round((Date.now() - entry.expiresAt) / 1000);
|
|
58022
|
-
|
|
58023
|
-
console.error(`[AUTH] VERIFY_FAIL: token expired ${expiredAgo}s ago (email=${claims.email}, team=${entry.teamId}, token=${token.slice(0, 12)}...)`);
|
|
58167
|
+
console.error(`[AUTH] VERIFY_FAIL: token expired ${expiredAgo}s ago (team=${entry.teamId})`);
|
|
58024
58168
|
await this.store.del("access_tokens", token);
|
|
58025
58169
|
throw new InvalidTokenError("Access token expired");
|
|
58026
58170
|
}
|
|
58171
|
+
const principal = readPrincipal(entry);
|
|
58027
58172
|
return {
|
|
58028
58173
|
token,
|
|
58029
58174
|
clientId: entry.clientId,
|
|
58030
58175
|
scopes: [],
|
|
58031
58176
|
expiresAt: Math.floor(entry.expiresAt / 1000),
|
|
58032
58177
|
extra: {
|
|
58033
|
-
|
|
58034
|
-
|
|
58035
|
-
|
|
58178
|
+
principal,
|
|
58179
|
+
...principal.type === "user" ? {
|
|
58180
|
+
supabaseJwt: principal.supabaseJwt,
|
|
58181
|
+
supabaseRefreshToken: principal.supabaseRefreshToken
|
|
58182
|
+
} : {},
|
|
58183
|
+
teamId: principal.teamId
|
|
58036
58184
|
}
|
|
58037
58185
|
};
|
|
58038
58186
|
}
|
|
@@ -58062,6 +58210,29 @@ class KadoaOAuthProvider {
|
|
|
58062
58210
|
res.redirect(redirectUrl.toString());
|
|
58063
58211
|
}
|
|
58064
58212
|
}
|
|
58213
|
+
async handleServiceAccountConsent(req, res) {
|
|
58214
|
+
const { state } = req.body;
|
|
58215
|
+
if (!state) {
|
|
58216
|
+
res.status(400).send("Missing state parameter");
|
|
58217
|
+
return;
|
|
58218
|
+
}
|
|
58219
|
+
const pending = await this.store.get("pending_auths", state);
|
|
58220
|
+
if (!pending) {
|
|
58221
|
+
res.status(400).send("Unknown or expired state parameter");
|
|
58222
|
+
return;
|
|
58223
|
+
}
|
|
58224
|
+
const config2 = getServiceAccountOAuthConfig();
|
|
58225
|
+
if (!config2 || pending.client.client_id !== config2.clientId) {
|
|
58226
|
+
res.status(403).send("Service-account authorization is not available for this client");
|
|
58227
|
+
return;
|
|
58228
|
+
}
|
|
58229
|
+
await this.store.del("pending_auths", state);
|
|
58230
|
+
await this.completeAuthFlow(pending, res, {
|
|
58231
|
+
type: "serviceAccount",
|
|
58232
|
+
credentialRef: config2.credentialRef,
|
|
58233
|
+
teamId: config2.teamId
|
|
58234
|
+
});
|
|
58235
|
+
}
|
|
58065
58236
|
async handleTeamSelection(req, res) {
|
|
58066
58237
|
const { token, teamId } = req.body;
|
|
58067
58238
|
if (!token || !teamId) {
|
|
@@ -58086,8 +58257,9 @@ class KadoaOAuthProvider {
|
|
|
58086
58257
|
try {
|
|
58087
58258
|
const refreshed = await setActiveTeamAndRefresh(entry.supabaseJwt, entry.supabaseRefreshToken, teamId);
|
|
58088
58259
|
await this.completeAuthFlow(entry.pending, res, {
|
|
58089
|
-
|
|
58090
|
-
|
|
58260
|
+
type: "user",
|
|
58261
|
+
supabaseJwt: refreshed.jwt,
|
|
58262
|
+
supabaseRefreshToken: refreshed.refreshToken,
|
|
58091
58263
|
teamId
|
|
58092
58264
|
});
|
|
58093
58265
|
} catch (error48) {
|
|
@@ -58101,12 +58273,10 @@ class KadoaOAuthProvider {
|
|
|
58101
58273
|
res.redirect(redirectUrl.toString());
|
|
58102
58274
|
}
|
|
58103
58275
|
}
|
|
58104
|
-
async completeAuthFlow(pending, res,
|
|
58276
|
+
async completeAuthFlow(pending, res, principal) {
|
|
58105
58277
|
const mcpCode = randomToken();
|
|
58106
58278
|
await this.store.set("auth_codes", mcpCode, {
|
|
58107
|
-
|
|
58108
|
-
supabaseRefreshToken: credentials.refreshToken,
|
|
58109
|
-
teamId: credentials.teamId,
|
|
58279
|
+
...persistPrincipal(principal),
|
|
58110
58280
|
codeChallenge: pending.params.codeChallenge,
|
|
58111
58281
|
clientId: pending.client.client_id,
|
|
58112
58282
|
redirectUri: pending.params.redirectUri,
|
|
@@ -58120,6 +58290,33 @@ class KadoaOAuthProvider {
|
|
|
58120
58290
|
res.redirect(redirectUrl.toString());
|
|
58121
58291
|
}
|
|
58122
58292
|
}
|
|
58293
|
+
function renderServiceAccountConsentPage(state, label) {
|
|
58294
|
+
return `<!DOCTYPE html>
|
|
58295
|
+
<html lang="en">
|
|
58296
|
+
<head>
|
|
58297
|
+
<meta charset="utf-8" />
|
|
58298
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
58299
|
+
<title>Connect shared Kadoa workspace</title>
|
|
58300
|
+
<style>
|
|
58301
|
+
body { font-family: ui-sans-serif, system-ui, sans-serif; background: #fafafa; color: #18181b; min-height: 100vh; display: grid; place-items: center; margin: 0; }
|
|
58302
|
+
main { width: min(420px, calc(100% - 2rem)); background: white; border: 1px solid #e4e4e7; border-radius: 8px; padding: 2rem; box-sizing: border-box; }
|
|
58303
|
+
h1 { font-size: 1.25rem; margin: 0 0 0.75rem; }
|
|
58304
|
+
p { color: #52525b; line-height: 1.5; }
|
|
58305
|
+
button { width: 100%; border: 1px solid #9a3412; border-radius: 4px; padding: 0.7rem 1rem; color: white; background: #c2410c; font: inherit; font-weight: 600; cursor: pointer; }
|
|
58306
|
+
</style>
|
|
58307
|
+
</head>
|
|
58308
|
+
<body>
|
|
58309
|
+
<main>
|
|
58310
|
+
<h1>Connect ${escapeHtml(label)}</h1>
|
|
58311
|
+
<p>Claude will use the shared Kadoa service account for this organization. Activity will not be attributed to your individual Kadoa identity.</p>
|
|
58312
|
+
<form method="POST" action="/auth/service-account">
|
|
58313
|
+
<input type="hidden" name="state" value="${escapeHtml(state)}" />
|
|
58314
|
+
<button type="submit">Continue with shared workspace</button>
|
|
58315
|
+
</form>
|
|
58316
|
+
</main>
|
|
58317
|
+
</body>
|
|
58318
|
+
</html>`;
|
|
58319
|
+
}
|
|
58123
58320
|
function renderTeamSelectionPage(teams, selectionToken) {
|
|
58124
58321
|
const teamButtons = teams.map((t) => `
|
|
58125
58322
|
<button type="submit" name="teamId" value="${t.id}" class="team-btn">
|
|
@@ -58542,6 +58739,7 @@ function escapeHtml(str) {
|
|
|
58542
58739
|
var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL;
|
|
58543
58740
|
var init_auth2 = __esm(() => {
|
|
58544
58741
|
init_errors4();
|
|
58742
|
+
init_service_account_config();
|
|
58545
58743
|
TEAM_SELECTION_TTL = 10 * 60 * 1000;
|
|
58546
58744
|
ACCESS_TOKEN_TTL = 7 * 24 * 3600;
|
|
58547
58745
|
});
|
|
@@ -58657,14 +58855,21 @@ function jwtClaims2(jwt2) {
|
|
|
58657
58855
|
}
|
|
58658
58856
|
function resolveAuth(req) {
|
|
58659
58857
|
const extra = req.auth?.extra;
|
|
58660
|
-
if (!extra) {
|
|
58661
|
-
console.error("[AUTH_RESOLVE] FAIL: req.auth.extra is missing");
|
|
58858
|
+
if (!extra || !extra.principal || typeof extra.principal !== "object") {
|
|
58859
|
+
console.error("[AUTH_RESOLVE] FAIL: req.auth.extra.principal is missing");
|
|
58662
58860
|
return;
|
|
58663
58861
|
}
|
|
58664
|
-
|
|
58665
|
-
|
|
58666
|
-
|
|
58667
|
-
|
|
58862
|
+
const principal = extra.principal;
|
|
58863
|
+
if (principal.type === "serviceAccount") {
|
|
58864
|
+
if (!principal.credentialRef || !principal.teamId) {
|
|
58865
|
+
console.error("[AUTH_RESOLVE] FAIL: service-account principal is incomplete");
|
|
58866
|
+
return;
|
|
58867
|
+
}
|
|
58868
|
+
return { principal, mcpToken: req.auth.token };
|
|
58869
|
+
}
|
|
58870
|
+
if (principal.type === "user") {
|
|
58871
|
+
const claims = jwtClaims2(principal.supabaseJwt);
|
|
58872
|
+
if (!claims.sub) {
|
|
58668
58873
|
console.error(`[AUTH_RESOLVE] FAIL: JWT missing sub claim (email=${claims.email ?? "unknown"})`);
|
|
58669
58874
|
return;
|
|
58670
58875
|
}
|
|
@@ -58672,20 +58877,14 @@ function resolveAuth(req) {
|
|
|
58672
58877
|
if (exp) {
|
|
58673
58878
|
const remainingSec = exp - Math.floor(Date.now() / 1000);
|
|
58674
58879
|
if (remainingSec <= 0) {
|
|
58675
|
-
console.error(`[AUTH_RESOLVE] WARN: JWT already expired ${-remainingSec}s ago (
|
|
58880
|
+
console.error(`[AUTH_RESOLVE] WARN: JWT already expired ${-remainingSec}s ago (team=${principal.teamId})`);
|
|
58676
58881
|
} else if (remainingSec < 300) {
|
|
58677
|
-
console.error(`[AUTH_RESOLVE] WARN: JWT expires in ${remainingSec}s (
|
|
58882
|
+
console.error(`[AUTH_RESOLVE] WARN: JWT expires in ${remainingSec}s (team=${principal.teamId})`);
|
|
58678
58883
|
}
|
|
58679
58884
|
}
|
|
58680
|
-
return {
|
|
58681
|
-
jwt: extra.supabaseJwt,
|
|
58682
|
-
refreshToken: extra.supabaseRefreshToken ?? "",
|
|
58683
|
-
teamId: extra.teamId ?? "",
|
|
58684
|
-
userId,
|
|
58685
|
-
mcpToken: req.auth.token
|
|
58686
|
-
};
|
|
58885
|
+
return { principal, mcpToken: req.auth.token };
|
|
58687
58886
|
}
|
|
58688
|
-
console.error(
|
|
58887
|
+
console.error("[AUTH_RESOLVE] FAIL: unknown principal type");
|
|
58689
58888
|
return;
|
|
58690
58889
|
}
|
|
58691
58890
|
async function startHttpServer(options) {
|
|
@@ -58710,6 +58909,9 @@ async function startHttpServer(options) {
|
|
|
58710
58909
|
app.post("/auth/login", express8.urlencoded({ extended: false }), (req, res) => {
|
|
58711
58910
|
provider.handleEmailPasswordLogin(req, res);
|
|
58712
58911
|
});
|
|
58912
|
+
app.post("/auth/service-account", express8.urlencoded({ extended: false }), (req, res) => {
|
|
58913
|
+
provider.handleServiceAccountConsent(req, res);
|
|
58914
|
+
});
|
|
58713
58915
|
app.post("/auth/sso", express8.urlencoded({ extended: false }), (req, res) => {
|
|
58714
58916
|
provider.handleSSOLogin(req, res);
|
|
58715
58917
|
});
|
|
@@ -58735,19 +58937,24 @@ async function startHttpServer(options) {
|
|
|
58735
58937
|
});
|
|
58736
58938
|
return;
|
|
58737
58939
|
}
|
|
58738
|
-
|
|
58739
|
-
|
|
58940
|
+
let principal = auth.principal;
|
|
58941
|
+
const identity2 = `mode=${principal.type}:team=${principal.teamId.slice(0, 8)}...`;
|
|
58942
|
+
if (principal.type === "user" && isJwtExpired(principal.supabaseJwt)) {
|
|
58740
58943
|
try {
|
|
58741
|
-
const refreshed = await refreshSupabaseJwtRaw(
|
|
58944
|
+
const refreshed = await refreshSupabaseJwtRaw(principal.supabaseRefreshToken);
|
|
58742
58945
|
if (refreshed) {
|
|
58743
|
-
|
|
58744
|
-
|
|
58946
|
+
principal = {
|
|
58947
|
+
...principal,
|
|
58948
|
+
supabaseJwt: refreshed.jwt,
|
|
58949
|
+
supabaseRefreshToken: refreshed.refreshToken
|
|
58950
|
+
};
|
|
58745
58951
|
const entry = await store.get("access_tokens", auth.mcpToken);
|
|
58746
58952
|
if (entry) {
|
|
58747
58953
|
const remainingMs = entry.expiresAt - Date.now();
|
|
58748
58954
|
if (remainingMs > 0) {
|
|
58749
58955
|
await store.set("access_tokens", auth.mcpToken, {
|
|
58750
58956
|
...entry,
|
|
58957
|
+
principal,
|
|
58751
58958
|
supabaseJwt: refreshed.jwt,
|
|
58752
58959
|
supabaseRefreshToken: refreshed.refreshToken
|
|
58753
58960
|
}, Math.ceil(remainingMs / 1000));
|
|
@@ -58774,31 +58981,32 @@ async function startHttpServer(options) {
|
|
|
58774
58981
|
const transport = new StreamableHTTPServerTransport({
|
|
58775
58982
|
sessionIdGenerator: undefined
|
|
58776
58983
|
});
|
|
58777
|
-
const
|
|
58778
|
-
|
|
58779
|
-
|
|
58780
|
-
|
|
58781
|
-
|
|
58782
|
-
const entry = await store.get("access_tokens", auth.mcpToken);
|
|
58783
|
-
if (!entry) {
|
|
58784
|
-
console.error(`[PERSIST] WARN: access token gone from store (token=${auth.mcpToken.slice(0, 12)}...)`);
|
|
58785
|
-
return;
|
|
58786
|
-
}
|
|
58787
|
-
const remainingMs = entry.expiresAt - Date.now();
|
|
58788
|
-
if (remainingMs <= 0) {
|
|
58789
|
-
console.error(`[PERSIST] WARN: access token already expired, skipping persist`);
|
|
58790
|
-
return;
|
|
58791
|
-
}
|
|
58792
|
-
const ttlSeconds = Math.ceil(remainingMs / 1000);
|
|
58793
|
-
await store.set("access_tokens", auth.mcpToken, {
|
|
58794
|
-
...entry,
|
|
58795
|
-
supabaseJwt: state.supabaseJwt ?? entry.supabaseJwt,
|
|
58796
|
-
supabaseRefreshToken: state.supabaseRefreshToken ?? entry.supabaseRefreshToken,
|
|
58797
|
-
teamId: state.teamId ?? entry.teamId
|
|
58798
|
-
}, ttlSeconds);
|
|
58799
|
-
console.error(`[PERSIST] OK: updated access token in store (token=${auth.mcpToken.slice(0, 12)}..., team=${state.teamId ?? "unchanged"}, ttl=${ttlSeconds}s)`);
|
|
58984
|
+
const persist = principal.type === "user" ? async (state) => {
|
|
58985
|
+
const entry = await store.get("access_tokens", auth.mcpToken);
|
|
58986
|
+
if (!entry) {
|
|
58987
|
+
console.error("[PERSIST] WARN: access token gone from store");
|
|
58988
|
+
return;
|
|
58800
58989
|
}
|
|
58801
|
-
|
|
58990
|
+
const remainingMs = entry.expiresAt - Date.now();
|
|
58991
|
+
if (remainingMs <= 0) {
|
|
58992
|
+
console.error("[PERSIST] WARN: access token already expired, skipping persist");
|
|
58993
|
+
return;
|
|
58994
|
+
}
|
|
58995
|
+
const updatedPrincipal = {
|
|
58996
|
+
type: "user",
|
|
58997
|
+
supabaseJwt: state.supabaseJwt ?? principal.supabaseJwt,
|
|
58998
|
+
supabaseRefreshToken: state.supabaseRefreshToken ?? principal.supabaseRefreshToken,
|
|
58999
|
+
teamId: state.teamId ?? principal.teamId
|
|
59000
|
+
};
|
|
59001
|
+
await store.set("access_tokens", auth.mcpToken, {
|
|
59002
|
+
...entry,
|
|
59003
|
+
principal: updatedPrincipal,
|
|
59004
|
+
supabaseJwt: updatedPrincipal.supabaseJwt,
|
|
59005
|
+
supabaseRefreshToken: updatedPrincipal.supabaseRefreshToken,
|
|
59006
|
+
teamId: updatedPrincipal.teamId
|
|
59007
|
+
}, Math.ceil(remainingMs / 1000));
|
|
59008
|
+
} : undefined;
|
|
59009
|
+
const server = await createServer({ principal, persist });
|
|
58802
59010
|
await server.connect(transport);
|
|
58803
59011
|
await transport.handleRequest(req, res, req.body);
|
|
58804
59012
|
} catch (error48) {
|
|
@@ -58852,11 +59060,18 @@ var init_http2 = __esm(async () => {
|
|
|
58852
59060
|
|
|
58853
59061
|
// src/index.ts
|
|
58854
59062
|
async function createServer(auth, options) {
|
|
58855
|
-
const
|
|
58856
|
-
|
|
59063
|
+
const principal = "principal" in auth ? auth.principal : {
|
|
59064
|
+
type: "user",
|
|
58857
59065
|
supabaseJwt: auth.jwt,
|
|
58858
59066
|
supabaseRefreshToken: auth.refreshToken,
|
|
58859
|
-
teamId: auth.teamId
|
|
59067
|
+
teamId: auth.teamId
|
|
59068
|
+
};
|
|
59069
|
+
const ctx = {
|
|
59070
|
+
client: createKadoaClient({ principal }),
|
|
59071
|
+
principal,
|
|
59072
|
+
supabaseJwt: principal.type === "user" ? principal.supabaseJwt : undefined,
|
|
59073
|
+
supabaseRefreshToken: principal.type === "user" ? principal.supabaseRefreshToken : undefined,
|
|
59074
|
+
teamId: principal.teamId,
|
|
58860
59075
|
persist: auth.persist
|
|
58861
59076
|
};
|
|
58862
59077
|
const server = new McpServer({ name: "kadoa", version: package_default.version }, {
|