@aiwerk/mcp-bridge 2.6.2 → 2.6.4
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/src/cli-auth.d.ts
CHANGED
|
@@ -41,4 +41,4 @@ export interface DeviceCodeConfig {
|
|
|
41
41
|
* 2. Display user_code and verification_uri to the user
|
|
42
42
|
* 3. Poll tokenUrl until the user authorizes or the code expires
|
|
43
43
|
*/
|
|
44
|
-
export declare function performDeviceCodeLogin(serverName: string, config: DeviceCodeConfig, logger: Logger): Promise<StoredToken>;
|
|
44
|
+
export declare function performDeviceCodeLogin(serverName: string, config: DeviceCodeConfig, logger: Logger, signal?: AbortSignal): Promise<StoredToken>;
|
package/dist/src/cli-auth.js
CHANGED
|
@@ -174,7 +174,7 @@ const SLOW_DOWN_INCREMENT_S = 5;
|
|
|
174
174
|
* 2. Display user_code and verification_uri to the user
|
|
175
175
|
* 3. Poll tokenUrl until the user authorizes or the code expires
|
|
176
176
|
*/
|
|
177
|
-
export async function performDeviceCodeLogin(serverName, config, logger) {
|
|
177
|
+
export async function performDeviceCodeLogin(serverName, config, logger, signal) {
|
|
178
178
|
// Step 1: Request device code
|
|
179
179
|
const formData = new URLSearchParams();
|
|
180
180
|
formData.set("client_id", config.clientId);
|
|
@@ -221,7 +221,13 @@ export async function performDeviceCodeLogin(serverName, config, logger) {
|
|
|
221
221
|
// Step 3: Poll for token
|
|
222
222
|
const deadline = Date.now() + expiresInS * 1000;
|
|
223
223
|
while (Date.now() < deadline) {
|
|
224
|
+
if (signal?.aborted) {
|
|
225
|
+
throw new Error("Device code login aborted");
|
|
226
|
+
}
|
|
224
227
|
await sleep(intervalS * 1000);
|
|
228
|
+
if (signal?.aborted) {
|
|
229
|
+
throw new Error("Device code login aborted");
|
|
230
|
+
}
|
|
225
231
|
const tokenForm = new URLSearchParams();
|
|
226
232
|
tokenForm.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
|
|
227
233
|
tokenForm.set("device_code", deviceCode);
|
|
@@ -232,6 +238,7 @@ export async function performDeviceCodeLogin(serverName, config, logger) {
|
|
|
232
238
|
method: "POST",
|
|
233
239
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
234
240
|
body: tokenForm.toString(),
|
|
241
|
+
signal,
|
|
235
242
|
});
|
|
236
243
|
// Guard against non-JSON responses (e.g. 500 HTML error pages)
|
|
237
244
|
const contentType = tokenResponse.headers.get("content-type") || "";
|
|
@@ -41,10 +41,16 @@ export declare class OAuth2TokenManager {
|
|
|
41
41
|
* Checks TokenStore, refreshes if expired, throws if unavailable.
|
|
42
42
|
*/
|
|
43
43
|
getTokenForDeviceCode(serverName: string, config: DeviceCodeOAuth2Config): Promise<string>;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Shared refresh logic for both auth_code and device_code flows.
|
|
46
|
+
* Takes a refreshFn that performs the actual token exchange.
|
|
47
|
+
*/
|
|
48
|
+
private doTokenRefresh;
|
|
49
|
+
/**
|
|
50
|
+
* Shared refresh token exchange for both auth_code and device_code flows.
|
|
51
|
+
* The only differences are which fields are optional (clientId/clientSecret).
|
|
52
|
+
*/
|
|
53
|
+
private refreshStoredToken;
|
|
48
54
|
private makeKey;
|
|
49
55
|
private fetchToken;
|
|
50
56
|
private exchangeToken;
|
|
@@ -66,7 +66,7 @@ export class OAuth2TokenManager {
|
|
|
66
66
|
if (existingInflight) {
|
|
67
67
|
return existingInflight;
|
|
68
68
|
}
|
|
69
|
-
const refreshPromise = this.
|
|
69
|
+
const refreshPromise = this.doTokenRefresh(serverName, stored, (s) => this.refreshStoredToken(s, config), "Auth code");
|
|
70
70
|
this.tokenRefreshInflight.set(serverName, refreshPromise);
|
|
71
71
|
try {
|
|
72
72
|
return await refreshPromise;
|
|
@@ -98,7 +98,7 @@ export class OAuth2TokenManager {
|
|
|
98
98
|
if (existingInflight) {
|
|
99
99
|
return existingInflight;
|
|
100
100
|
}
|
|
101
|
-
const refreshPromise = this.
|
|
101
|
+
const refreshPromise = this.doTokenRefresh(serverName, stored, (s) => this.refreshStoredToken(s, config), "Device code");
|
|
102
102
|
this.tokenRefreshInflight.set(serverName, refreshPromise);
|
|
103
103
|
try {
|
|
104
104
|
return await refreshPromise;
|
|
@@ -107,66 +107,19 @@ export class OAuth2TokenManager {
|
|
|
107
107
|
this.tokenRefreshInflight.delete(serverName);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return refreshed.accessToken;
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
this.logger.warn("[mcp-bridge] Device code token refresh failed:", err);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
// Refresh failed or no refresh token
|
|
122
|
-
this.tokenStore.remove(serverName);
|
|
123
|
-
const error = new Error(`Authentication expired for server "${serverName}". Run: mcp-bridge auth login ${serverName}`);
|
|
124
|
-
error.code = -32006;
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
async refreshDeviceCodeToken(stored, config) {
|
|
128
|
-
const formData = new URLSearchParams();
|
|
129
|
-
formData.set("grant_type", "refresh_token");
|
|
130
|
-
formData.set("refresh_token", stored.refreshToken);
|
|
131
|
-
formData.set("client_id", config.clientId);
|
|
132
|
-
if (config.clientSecret)
|
|
133
|
-
formData.set("client_secret", config.clientSecret);
|
|
134
|
-
if (config.scopes?.length)
|
|
135
|
-
formData.set("scope", config.scopes.join(" "));
|
|
136
|
-
const response = await fetch(stored.tokenUrl, {
|
|
137
|
-
method: "POST",
|
|
138
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
139
|
-
body: formData.toString(),
|
|
140
|
-
});
|
|
141
|
-
if (!response.ok) {
|
|
142
|
-
throw new Error(`OAuth2 refresh token exchange failed: HTTP ${response.status}`);
|
|
143
|
-
}
|
|
144
|
-
const payload = (await response.json());
|
|
145
|
-
if (!payload.access_token) {
|
|
146
|
-
throw new Error("OAuth2 refresh response missing access_token");
|
|
147
|
-
}
|
|
148
|
-
const expiresIn = Number.isFinite(payload.expires_in)
|
|
149
|
-
? Number(payload.expires_in)
|
|
150
|
-
: DEFAULT_EXPIRES_IN_SECONDS;
|
|
151
|
-
const expiresAt = Date.now() + Math.max(0, expiresIn - EXPIRY_BUFFER_SECONDS) * 1000;
|
|
152
|
-
return {
|
|
153
|
-
accessToken: payload.access_token,
|
|
154
|
-
refreshToken: payload.refresh_token ?? stored.refreshToken,
|
|
155
|
-
expiresAt,
|
|
156
|
-
tokenUrl: stored.tokenUrl,
|
|
157
|
-
clientId: config.clientId,
|
|
158
|
-
scopes: config.scopes,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
async doAuthCodeRefresh(serverName, stored, config) {
|
|
110
|
+
/**
|
|
111
|
+
* Shared refresh logic for both auth_code and device_code flows.
|
|
112
|
+
* Takes a refreshFn that performs the actual token exchange.
|
|
113
|
+
*/
|
|
114
|
+
async doTokenRefresh(serverName, stored, refreshFn, flowName) {
|
|
162
115
|
if (stored.refreshToken) {
|
|
163
116
|
try {
|
|
164
|
-
const refreshed = await
|
|
117
|
+
const refreshed = await refreshFn(stored);
|
|
165
118
|
this.tokenStore.save(serverName, refreshed);
|
|
166
119
|
return refreshed.accessToken;
|
|
167
120
|
}
|
|
168
121
|
catch (err) {
|
|
169
|
-
this.logger.warn(
|
|
122
|
+
this.logger.warn(`[mcp-bridge] ${flowName} token refresh failed:`, err);
|
|
170
123
|
}
|
|
171
124
|
}
|
|
172
125
|
// Refresh failed or no refresh token
|
|
@@ -175,16 +128,20 @@ export class OAuth2TokenManager {
|
|
|
175
128
|
error.code = -32006;
|
|
176
129
|
throw error;
|
|
177
130
|
}
|
|
178
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Shared refresh token exchange for both auth_code and device_code flows.
|
|
133
|
+
* The only differences are which fields are optional (clientId/clientSecret).
|
|
134
|
+
*/
|
|
135
|
+
async refreshStoredToken(stored, params) {
|
|
179
136
|
const formData = new URLSearchParams();
|
|
180
137
|
formData.set("grant_type", "refresh_token");
|
|
181
138
|
formData.set("refresh_token", stored.refreshToken);
|
|
182
|
-
if (
|
|
183
|
-
formData.set("client_id",
|
|
184
|
-
if (
|
|
185
|
-
formData.set("client_secret",
|
|
186
|
-
if (
|
|
187
|
-
formData.set("scope",
|
|
139
|
+
if (params.clientId)
|
|
140
|
+
formData.set("client_id", params.clientId);
|
|
141
|
+
if (params.clientSecret)
|
|
142
|
+
formData.set("client_secret", params.clientSecret);
|
|
143
|
+
if (params.scopes?.length)
|
|
144
|
+
formData.set("scope", params.scopes.join(" "));
|
|
188
145
|
const response = await fetch(stored.tokenUrl, {
|
|
189
146
|
method: "POST",
|
|
190
147
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -206,8 +163,8 @@ export class OAuth2TokenManager {
|
|
|
206
163
|
refreshToken: payload.refresh_token ?? stored.refreshToken,
|
|
207
164
|
expiresAt,
|
|
208
165
|
tokenUrl: stored.tokenUrl,
|
|
209
|
-
clientId:
|
|
210
|
-
scopes:
|
|
166
|
+
clientId: params.clientId,
|
|
167
|
+
scopes: params.scopes,
|
|
211
168
|
};
|
|
212
169
|
}
|
|
213
170
|
makeKey(tokenUrl, clientId) {
|