@lanonasis/oauth-client 1.0.2 → 1.2.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/README.md +31 -2
- package/dist/index.cjs +132 -29
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +132 -29
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# @lanonasis/oauth-client
|
|
2
2
|
|
|
3
|
-
Drop-in OAuth +
|
|
3
|
+
Drop-in OAuth + API Key authentication client for the Lanonasis ecosystem. Supports dual authentication modes: OAuth2 PKCE flow for pre-registered clients and direct API key authentication for dashboard users. Handles browser/desktop/terminal flows, token lifecycle, secure storage, and MCP WebSocket/SSE connections.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
|
+
- **Dual Authentication**: OAuth2 PKCE flow OR direct API key authentication
|
|
6
7
|
- OAuth flows for terminal and desktop (Electron-friendly) environments
|
|
8
|
+
- API key authentication for new users with dashboard-generated keys
|
|
7
9
|
- Token storage with secure backends (Keytar, encrypted files, Electron secure store, mobile secure storage, WebCrypto in browsers)
|
|
8
10
|
- API key storage that normalizes to SHA-256 digests before persisting
|
|
9
11
|
- MCP client that connects over WebSocket (`/ws`) or SSE (`/sse`) with auto-refreshing tokens
|
|
12
|
+
- Automatic auth mode detection based on configuration
|
|
10
13
|
- ESM + CJS bundles with TypeScript types
|
|
11
14
|
|
|
12
15
|
## Installation
|
|
@@ -17,6 +20,24 @@ bun add @lanonasis/oauth-client
|
|
|
17
20
|
```
|
|
18
21
|
|
|
19
22
|
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Option 1: API Key Authentication (Recommended for New Users)
|
|
25
|
+
```ts
|
|
26
|
+
import { MCPClient } from '@lanonasis/oauth-client';
|
|
27
|
+
|
|
28
|
+
// Simple API key mode - perfect for dashboard users
|
|
29
|
+
const client = new MCPClient({
|
|
30
|
+
apiKey: 'lano_abc123xyz', // Get from dashboard
|
|
31
|
+
mcpEndpoint: 'wss://mcp.lanonasis.com'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await client.connect(); // Automatically uses API key auth
|
|
35
|
+
|
|
36
|
+
// Make requests
|
|
37
|
+
const memories = await client.searchMemories('test query');
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Option 2: OAuth Authentication (For Pre-registered Clients)
|
|
20
41
|
```ts
|
|
21
42
|
import { MCPClient } from '@lanonasis/oauth-client';
|
|
22
43
|
|
|
@@ -27,7 +48,7 @@ const client = new MCPClient({
|
|
|
27
48
|
scope: 'mcp:read mcp:write api_keys:manage'
|
|
28
49
|
});
|
|
29
50
|
|
|
30
|
-
await client.connect(); //
|
|
51
|
+
await client.connect(); // Triggers OAuth flow, handles refresh
|
|
31
52
|
```
|
|
32
53
|
|
|
33
54
|
### Terminal OAuth flow
|
|
@@ -74,12 +95,20 @@ const hashed = await apiKeys.getApiKey(); // returns sha256 hex digest
|
|
|
74
95
|
```
|
|
75
96
|
|
|
76
97
|
## Configuration
|
|
98
|
+
|
|
99
|
+
### API Key Mode
|
|
100
|
+
- `apiKey` (required): Your dashboard-generated API key (starts with `lano_`).
|
|
101
|
+
- `mcpEndpoint` (optional): defaults to `wss://mcp.lanonasis.com` and can also be `https://...` for SSE.
|
|
102
|
+
|
|
103
|
+
### OAuth Mode
|
|
77
104
|
- `clientId` (required): OAuth client id issued by Lanonasis Auth.
|
|
78
105
|
- `authBaseUrl` (optional): defaults to `https://auth.lanonasis.com`.
|
|
79
106
|
- `mcpEndpoint` (optional): defaults to `wss://mcp.lanonasis.com` and can also be `https://...` for SSE.
|
|
80
107
|
- `scope` (optional): defaults to `mcp:read mcp:write api_keys:manage`.
|
|
81
108
|
- `autoRefresh` (MCPClient): refresh tokens 5 minutes before expiry (default `true`).
|
|
82
109
|
|
|
110
|
+
**Note**: Auth mode is automatically detected - if you provide `apiKey`, it uses API key authentication. If you provide `clientId`, it uses OAuth.
|
|
111
|
+
|
|
83
112
|
## Publishing (maintainers)
|
|
84
113
|
1) Build artifacts: `npm install && npm run build`
|
|
85
114
|
2) Verify contents: ensure `dist`, `README.md`, `LICENSE` are present.
|
package/dist/index.cjs
CHANGED
|
@@ -440,6 +440,9 @@ var TokenStorage = class {
|
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
442
|
isTokenExpired(tokens) {
|
|
443
|
+
if (tokens.token_type === "api-key" || tokens.expires_in === 0) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
443
446
|
if (!tokens.expires_in) return false;
|
|
444
447
|
if (!tokens.issued_at) {
|
|
445
448
|
console.warn("Token missing issued_at timestamp, treating as expired");
|
|
@@ -1071,9 +1074,66 @@ var ApiKeyStorage = class {
|
|
|
1071
1074
|
}
|
|
1072
1075
|
};
|
|
1073
1076
|
|
|
1077
|
+
// src/flows/apikey-flow.ts
|
|
1078
|
+
var APIKeyFlow = class extends BaseOAuthFlow {
|
|
1079
|
+
constructor(apiKey, authBaseUrl = "https://mcp.lanonasis.com") {
|
|
1080
|
+
super({
|
|
1081
|
+
clientId: "api-key-client",
|
|
1082
|
+
authBaseUrl
|
|
1083
|
+
});
|
|
1084
|
+
this.apiKey = apiKey;
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* "Authenticate" by returning the API key as a virtual token
|
|
1088
|
+
* The API key will be used directly in request headers
|
|
1089
|
+
*/
|
|
1090
|
+
async authenticate() {
|
|
1091
|
+
if (!this.apiKey || !this.apiKey.startsWith("lano_") && !this.apiKey.startsWith("vx_")) {
|
|
1092
|
+
throw new Error(
|
|
1093
|
+
'Invalid API key format. Must start with "lano_" or "vx_". Please regenerate your API key from the dashboard.'
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
if (this.apiKey.startsWith("vx_")) {
|
|
1097
|
+
console.warn(
|
|
1098
|
+
'\u26A0\uFE0F DEPRECATION WARNING: API keys with "vx_" prefix are deprecated and will stop working soon. Please regenerate your API key from the dashboard to get a "lano_" prefixed key. Support for "vx_" keys will be removed in a future version.'
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
access_token: this.apiKey,
|
|
1103
|
+
token_type: "api-key",
|
|
1104
|
+
expires_in: 0,
|
|
1105
|
+
// API keys don't expire
|
|
1106
|
+
issued_at: Date.now()
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* API keys don't need refresh
|
|
1111
|
+
*/
|
|
1112
|
+
async refreshToken(refreshToken) {
|
|
1113
|
+
throw new Error("API keys do not support token refresh");
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Optional: Validate API key by making a test request
|
|
1117
|
+
*/
|
|
1118
|
+
async validateAPIKey() {
|
|
1119
|
+
try {
|
|
1120
|
+
const response = await fetch(`${this.config.authBaseUrl}/api/v1/health`, {
|
|
1121
|
+
headers: {
|
|
1122
|
+
"x-api-key": this.apiKey
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
return response.ok;
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
console.error("API key validation failed:", error);
|
|
1128
|
+
return false;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1074
1133
|
// src/client/mcp-client.ts
|
|
1075
1134
|
var MCPClient = class {
|
|
1076
1135
|
constructor(config = {}) {
|
|
1136
|
+
// ← NEW: Track auth mode
|
|
1077
1137
|
this.ws = null;
|
|
1078
1138
|
this.eventSource = null;
|
|
1079
1139
|
this.accessToken = null;
|
|
@@ -1084,30 +1144,45 @@ var MCPClient = class {
|
|
|
1084
1144
|
...config
|
|
1085
1145
|
};
|
|
1086
1146
|
this.tokenStorage = new TokenStorage();
|
|
1087
|
-
|
|
1088
|
-
|
|
1147
|
+
this.authMode = config.apiKey ? "apikey" : "oauth";
|
|
1148
|
+
if (this.authMode === "apikey") {
|
|
1149
|
+
this.authFlow = new APIKeyFlow(
|
|
1150
|
+
config.apiKey,
|
|
1151
|
+
config.authBaseUrl || "https://mcp.lanonasis.com"
|
|
1152
|
+
);
|
|
1089
1153
|
} else {
|
|
1090
|
-
this.
|
|
1154
|
+
if (this.isTerminal()) {
|
|
1155
|
+
this.authFlow = new TerminalOAuthFlow(config);
|
|
1156
|
+
} else {
|
|
1157
|
+
this.authFlow = new DesktopOAuthFlow(config);
|
|
1158
|
+
}
|
|
1091
1159
|
}
|
|
1092
1160
|
}
|
|
1093
1161
|
async connect() {
|
|
1094
1162
|
try {
|
|
1095
1163
|
let tokens = await this.tokenStorage.retrieve();
|
|
1096
|
-
if (
|
|
1097
|
-
if (tokens
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1164
|
+
if (this.authMode === "apikey") {
|
|
1165
|
+
if (!tokens) {
|
|
1166
|
+
tokens = await this.authenticate();
|
|
1167
|
+
}
|
|
1168
|
+
this.accessToken = tokens.access_token;
|
|
1169
|
+
} else {
|
|
1170
|
+
if (!tokens || this.tokenStorage.isTokenExpired(tokens)) {
|
|
1171
|
+
if (tokens?.refresh_token) {
|
|
1172
|
+
try {
|
|
1173
|
+
tokens = await this.authFlow.refreshToken(tokens.refresh_token);
|
|
1174
|
+
await this.tokenStorage.store(tokens);
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
tokens = await this.authenticate();
|
|
1177
|
+
}
|
|
1178
|
+
} else {
|
|
1102
1179
|
tokens = await this.authenticate();
|
|
1103
1180
|
}
|
|
1104
|
-
} else {
|
|
1105
|
-
tokens = await this.authenticate();
|
|
1106
1181
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1182
|
+
this.accessToken = tokens.access_token;
|
|
1183
|
+
if (this.config.autoRefresh && tokens.expires_in) {
|
|
1184
|
+
this.scheduleTokenRefresh(tokens);
|
|
1185
|
+
}
|
|
1111
1186
|
}
|
|
1112
1187
|
await this.establishConnection();
|
|
1113
1188
|
} catch (error) {
|
|
@@ -1127,6 +1202,10 @@ var MCPClient = class {
|
|
|
1127
1202
|
if (!tokens) {
|
|
1128
1203
|
throw new Error("Not authenticated");
|
|
1129
1204
|
}
|
|
1205
|
+
if (this.authMode === "apikey") {
|
|
1206
|
+
this.accessToken = tokens.access_token;
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1130
1209
|
if (this.tokenStorage.isTokenExpired(tokens)) {
|
|
1131
1210
|
if (tokens.refresh_token) {
|
|
1132
1211
|
try {
|
|
@@ -1183,11 +1262,19 @@ var MCPClient = class {
|
|
|
1183
1262
|
this.ws = new WebSocket(wsUrl.toString());
|
|
1184
1263
|
} else {
|
|
1185
1264
|
const { default: WS } = await import("ws");
|
|
1186
|
-
this.
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1265
|
+
if (this.authMode === "apikey") {
|
|
1266
|
+
this.ws = new WS(wsUrl.toString(), {
|
|
1267
|
+
headers: {
|
|
1268
|
+
"x-api-key": this.accessToken
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
} else {
|
|
1272
|
+
this.ws = new WS(wsUrl.toString(), {
|
|
1273
|
+
headers: {
|
|
1274
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1191
1278
|
}
|
|
1192
1279
|
return new Promise((resolve, reject) => {
|
|
1193
1280
|
if (!this.ws) {
|
|
@@ -1221,11 +1308,19 @@ var MCPClient = class {
|
|
|
1221
1308
|
} else {
|
|
1222
1309
|
const EventSourceModule = await import("eventsource");
|
|
1223
1310
|
const ES = EventSourceModule.default || EventSourceModule;
|
|
1224
|
-
this.
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1311
|
+
if (this.authMode === "apikey") {
|
|
1312
|
+
this.eventSource = new ES(sseUrl.toString(), {
|
|
1313
|
+
headers: {
|
|
1314
|
+
"x-api-key": this.accessToken
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
} else {
|
|
1318
|
+
this.eventSource = new ES(sseUrl.toString(), {
|
|
1319
|
+
headers: {
|
|
1320
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1229
1324
|
}
|
|
1230
1325
|
this.eventSource.onopen = () => {
|
|
1231
1326
|
console.log("MCP SSE connected");
|
|
@@ -1255,12 +1350,17 @@ var MCPClient = class {
|
|
|
1255
1350
|
if (!this.accessToken) {
|
|
1256
1351
|
throw new Error("Not authenticated");
|
|
1257
1352
|
}
|
|
1353
|
+
const headers = {
|
|
1354
|
+
"Content-Type": "application/json"
|
|
1355
|
+
};
|
|
1356
|
+
if (this.authMode === "apikey") {
|
|
1357
|
+
headers["x-api-key"] = this.accessToken;
|
|
1358
|
+
} else {
|
|
1359
|
+
headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
1360
|
+
}
|
|
1258
1361
|
const response = await fetch(`${this.config.mcpEndpoint}/api`, {
|
|
1259
1362
|
method: "POST",
|
|
1260
|
-
headers
|
|
1261
|
-
"Authorization": `Bearer ${this.accessToken}`,
|
|
1262
|
-
"Content-Type": "application/json"
|
|
1263
|
-
},
|
|
1363
|
+
headers,
|
|
1264
1364
|
body: JSON.stringify({
|
|
1265
1365
|
jsonrpc: "2.0",
|
|
1266
1366
|
id: this.generateId(),
|
|
@@ -1269,6 +1369,9 @@ var MCPClient = class {
|
|
|
1269
1369
|
})
|
|
1270
1370
|
});
|
|
1271
1371
|
if (response.status === 401) {
|
|
1372
|
+
if (this.authMode === "apikey") {
|
|
1373
|
+
throw new Error("Invalid API key - please check your credentials");
|
|
1374
|
+
}
|
|
1272
1375
|
const tokens = await this.tokenStorage.retrieve();
|
|
1273
1376
|
if (tokens?.refresh_token) {
|
|
1274
1377
|
const newTokens = await this.authFlow.refreshToken(tokens.refresh_token);
|
package/dist/index.d.cts
CHANGED
|
@@ -172,11 +172,13 @@ declare class ApiKeyStorage {
|
|
|
172
172
|
interface MCPClientConfig extends Partial<OAuthConfig> {
|
|
173
173
|
mcpEndpoint?: string;
|
|
174
174
|
autoRefresh?: boolean;
|
|
175
|
+
apiKey?: string;
|
|
175
176
|
}
|
|
176
177
|
declare class MCPClient {
|
|
177
178
|
private tokenStorage;
|
|
178
179
|
private authFlow;
|
|
179
180
|
private config;
|
|
181
|
+
private authMode;
|
|
180
182
|
private ws;
|
|
181
183
|
private eventSource;
|
|
182
184
|
private accessToken;
|
package/dist/index.d.ts
CHANGED
|
@@ -172,11 +172,13 @@ declare class ApiKeyStorage {
|
|
|
172
172
|
interface MCPClientConfig extends Partial<OAuthConfig> {
|
|
173
173
|
mcpEndpoint?: string;
|
|
174
174
|
autoRefresh?: boolean;
|
|
175
|
+
apiKey?: string;
|
|
175
176
|
}
|
|
176
177
|
declare class MCPClient {
|
|
177
178
|
private tokenStorage;
|
|
178
179
|
private authFlow;
|
|
179
180
|
private config;
|
|
181
|
+
private authMode;
|
|
180
182
|
private ws;
|
|
181
183
|
private eventSource;
|
|
182
184
|
private accessToken;
|
package/dist/index.js
CHANGED
|
@@ -406,6 +406,9 @@ var TokenStorage = class {
|
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
408
|
isTokenExpired(tokens) {
|
|
409
|
+
if (tokens.token_type === "api-key" || tokens.expires_in === 0) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
409
412
|
if (!tokens.expires_in) return false;
|
|
410
413
|
if (!tokens.issued_at) {
|
|
411
414
|
console.warn("Token missing issued_at timestamp, treating as expired");
|
|
@@ -1037,9 +1040,66 @@ var ApiKeyStorage = class {
|
|
|
1037
1040
|
}
|
|
1038
1041
|
};
|
|
1039
1042
|
|
|
1043
|
+
// src/flows/apikey-flow.ts
|
|
1044
|
+
var APIKeyFlow = class extends BaseOAuthFlow {
|
|
1045
|
+
constructor(apiKey, authBaseUrl = "https://mcp.lanonasis.com") {
|
|
1046
|
+
super({
|
|
1047
|
+
clientId: "api-key-client",
|
|
1048
|
+
authBaseUrl
|
|
1049
|
+
});
|
|
1050
|
+
this.apiKey = apiKey;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* "Authenticate" by returning the API key as a virtual token
|
|
1054
|
+
* The API key will be used directly in request headers
|
|
1055
|
+
*/
|
|
1056
|
+
async authenticate() {
|
|
1057
|
+
if (!this.apiKey || !this.apiKey.startsWith("lano_") && !this.apiKey.startsWith("vx_")) {
|
|
1058
|
+
throw new Error(
|
|
1059
|
+
'Invalid API key format. Must start with "lano_" or "vx_". Please regenerate your API key from the dashboard.'
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
if (this.apiKey.startsWith("vx_")) {
|
|
1063
|
+
console.warn(
|
|
1064
|
+
'\u26A0\uFE0F DEPRECATION WARNING: API keys with "vx_" prefix are deprecated and will stop working soon. Please regenerate your API key from the dashboard to get a "lano_" prefixed key. Support for "vx_" keys will be removed in a future version.'
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
return {
|
|
1068
|
+
access_token: this.apiKey,
|
|
1069
|
+
token_type: "api-key",
|
|
1070
|
+
expires_in: 0,
|
|
1071
|
+
// API keys don't expire
|
|
1072
|
+
issued_at: Date.now()
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* API keys don't need refresh
|
|
1077
|
+
*/
|
|
1078
|
+
async refreshToken(refreshToken) {
|
|
1079
|
+
throw new Error("API keys do not support token refresh");
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Optional: Validate API key by making a test request
|
|
1083
|
+
*/
|
|
1084
|
+
async validateAPIKey() {
|
|
1085
|
+
try {
|
|
1086
|
+
const response = await fetch(`${this.config.authBaseUrl}/api/v1/health`, {
|
|
1087
|
+
headers: {
|
|
1088
|
+
"x-api-key": this.apiKey
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
return response.ok;
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
console.error("API key validation failed:", error);
|
|
1094
|
+
return false;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1040
1099
|
// src/client/mcp-client.ts
|
|
1041
1100
|
var MCPClient = class {
|
|
1042
1101
|
constructor(config = {}) {
|
|
1102
|
+
// ← NEW: Track auth mode
|
|
1043
1103
|
this.ws = null;
|
|
1044
1104
|
this.eventSource = null;
|
|
1045
1105
|
this.accessToken = null;
|
|
@@ -1050,30 +1110,45 @@ var MCPClient = class {
|
|
|
1050
1110
|
...config
|
|
1051
1111
|
};
|
|
1052
1112
|
this.tokenStorage = new TokenStorage();
|
|
1053
|
-
|
|
1054
|
-
|
|
1113
|
+
this.authMode = config.apiKey ? "apikey" : "oauth";
|
|
1114
|
+
if (this.authMode === "apikey") {
|
|
1115
|
+
this.authFlow = new APIKeyFlow(
|
|
1116
|
+
config.apiKey,
|
|
1117
|
+
config.authBaseUrl || "https://mcp.lanonasis.com"
|
|
1118
|
+
);
|
|
1055
1119
|
} else {
|
|
1056
|
-
this.
|
|
1120
|
+
if (this.isTerminal()) {
|
|
1121
|
+
this.authFlow = new TerminalOAuthFlow(config);
|
|
1122
|
+
} else {
|
|
1123
|
+
this.authFlow = new DesktopOAuthFlow(config);
|
|
1124
|
+
}
|
|
1057
1125
|
}
|
|
1058
1126
|
}
|
|
1059
1127
|
async connect() {
|
|
1060
1128
|
try {
|
|
1061
1129
|
let tokens = await this.tokenStorage.retrieve();
|
|
1062
|
-
if (
|
|
1063
|
-
if (tokens
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1130
|
+
if (this.authMode === "apikey") {
|
|
1131
|
+
if (!tokens) {
|
|
1132
|
+
tokens = await this.authenticate();
|
|
1133
|
+
}
|
|
1134
|
+
this.accessToken = tokens.access_token;
|
|
1135
|
+
} else {
|
|
1136
|
+
if (!tokens || this.tokenStorage.isTokenExpired(tokens)) {
|
|
1137
|
+
if (tokens?.refresh_token) {
|
|
1138
|
+
try {
|
|
1139
|
+
tokens = await this.authFlow.refreshToken(tokens.refresh_token);
|
|
1140
|
+
await this.tokenStorage.store(tokens);
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
tokens = await this.authenticate();
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1068
1145
|
tokens = await this.authenticate();
|
|
1069
1146
|
}
|
|
1070
|
-
} else {
|
|
1071
|
-
tokens = await this.authenticate();
|
|
1072
1147
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1148
|
+
this.accessToken = tokens.access_token;
|
|
1149
|
+
if (this.config.autoRefresh && tokens.expires_in) {
|
|
1150
|
+
this.scheduleTokenRefresh(tokens);
|
|
1151
|
+
}
|
|
1077
1152
|
}
|
|
1078
1153
|
await this.establishConnection();
|
|
1079
1154
|
} catch (error) {
|
|
@@ -1093,6 +1168,10 @@ var MCPClient = class {
|
|
|
1093
1168
|
if (!tokens) {
|
|
1094
1169
|
throw new Error("Not authenticated");
|
|
1095
1170
|
}
|
|
1171
|
+
if (this.authMode === "apikey") {
|
|
1172
|
+
this.accessToken = tokens.access_token;
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1096
1175
|
if (this.tokenStorage.isTokenExpired(tokens)) {
|
|
1097
1176
|
if (tokens.refresh_token) {
|
|
1098
1177
|
try {
|
|
@@ -1149,11 +1228,19 @@ var MCPClient = class {
|
|
|
1149
1228
|
this.ws = new WebSocket(wsUrl.toString());
|
|
1150
1229
|
} else {
|
|
1151
1230
|
const { default: WS } = await import("ws");
|
|
1152
|
-
this.
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1231
|
+
if (this.authMode === "apikey") {
|
|
1232
|
+
this.ws = new WS(wsUrl.toString(), {
|
|
1233
|
+
headers: {
|
|
1234
|
+
"x-api-key": this.accessToken
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
} else {
|
|
1238
|
+
this.ws = new WS(wsUrl.toString(), {
|
|
1239
|
+
headers: {
|
|
1240
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1157
1244
|
}
|
|
1158
1245
|
return new Promise((resolve, reject) => {
|
|
1159
1246
|
if (!this.ws) {
|
|
@@ -1187,11 +1274,19 @@ var MCPClient = class {
|
|
|
1187
1274
|
} else {
|
|
1188
1275
|
const EventSourceModule = await import("eventsource");
|
|
1189
1276
|
const ES = EventSourceModule.default || EventSourceModule;
|
|
1190
|
-
this.
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1277
|
+
if (this.authMode === "apikey") {
|
|
1278
|
+
this.eventSource = new ES(sseUrl.toString(), {
|
|
1279
|
+
headers: {
|
|
1280
|
+
"x-api-key": this.accessToken
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
} else {
|
|
1284
|
+
this.eventSource = new ES(sseUrl.toString(), {
|
|
1285
|
+
headers: {
|
|
1286
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1195
1290
|
}
|
|
1196
1291
|
this.eventSource.onopen = () => {
|
|
1197
1292
|
console.log("MCP SSE connected");
|
|
@@ -1221,12 +1316,17 @@ var MCPClient = class {
|
|
|
1221
1316
|
if (!this.accessToken) {
|
|
1222
1317
|
throw new Error("Not authenticated");
|
|
1223
1318
|
}
|
|
1319
|
+
const headers = {
|
|
1320
|
+
"Content-Type": "application/json"
|
|
1321
|
+
};
|
|
1322
|
+
if (this.authMode === "apikey") {
|
|
1323
|
+
headers["x-api-key"] = this.accessToken;
|
|
1324
|
+
} else {
|
|
1325
|
+
headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
1326
|
+
}
|
|
1224
1327
|
const response = await fetch(`${this.config.mcpEndpoint}/api`, {
|
|
1225
1328
|
method: "POST",
|
|
1226
|
-
headers
|
|
1227
|
-
"Authorization": `Bearer ${this.accessToken}`,
|
|
1228
|
-
"Content-Type": "application/json"
|
|
1229
|
-
},
|
|
1329
|
+
headers,
|
|
1230
1330
|
body: JSON.stringify({
|
|
1231
1331
|
jsonrpc: "2.0",
|
|
1232
1332
|
id: this.generateId(),
|
|
@@ -1235,6 +1335,9 @@ var MCPClient = class {
|
|
|
1235
1335
|
})
|
|
1236
1336
|
});
|
|
1237
1337
|
if (response.status === 401) {
|
|
1338
|
+
if (this.authMode === "apikey") {
|
|
1339
|
+
throw new Error("Invalid API key - please check your credentials");
|
|
1340
|
+
}
|
|
1238
1341
|
const tokens = await this.tokenStorage.retrieve();
|
|
1239
1342
|
if (tokens?.refresh_token) {
|
|
1240
1343
|
const newTokens = await this.authFlow.refreshToken(tokens.refresh_token);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lanonasis/oauth-client",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "OAuth client for Lan Onasis MCP integration",
|
|
5
|
+
"description": "OAuth and API Key authentication client for Lan Onasis MCP integration",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Lan Onasis",
|
|
8
8
|
"main": "./dist/index.js",
|