@lanonasis/oauth-client 1.0.1 → 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 +167 -37
- package/dist/index.d.cts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +167 -37
- package/package.json +4 -5
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
|
@@ -378,7 +378,11 @@ var TokenStorage = class {
|
|
|
378
378
|
}
|
|
379
379
|
}
|
|
380
380
|
async store(tokens) {
|
|
381
|
-
const
|
|
381
|
+
const tokensWithTimestamp = {
|
|
382
|
+
...tokens,
|
|
383
|
+
issued_at: Date.now()
|
|
384
|
+
};
|
|
385
|
+
const tokenString = JSON.stringify(tokensWithTimestamp);
|
|
382
386
|
if (this.isNode()) {
|
|
383
387
|
if (this.keytar) {
|
|
384
388
|
await this.keytar.setPassword("lanonasis-mcp", "tokens", tokenString);
|
|
@@ -386,7 +390,7 @@ var TokenStorage = class {
|
|
|
386
390
|
await this.storeToFile(tokenString);
|
|
387
391
|
}
|
|
388
392
|
} else if (this.isElectron()) {
|
|
389
|
-
await window.electronAPI.secureStore.set(this.storageKey,
|
|
393
|
+
await window.electronAPI.secureStore.set(this.storageKey, tokensWithTimestamp);
|
|
390
394
|
} else if (this.isMobile()) {
|
|
391
395
|
await window.SecureStorage.set(this.storageKey, tokenString);
|
|
392
396
|
} else {
|
|
@@ -436,16 +440,18 @@ var TokenStorage = class {
|
|
|
436
440
|
}
|
|
437
441
|
}
|
|
438
442
|
isTokenExpired(tokens) {
|
|
443
|
+
if (tokens.token_type === "api-key" || tokens.expires_in === 0) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
439
446
|
if (!tokens.expires_in) return false;
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
447
|
+
if (!tokens.issued_at) {
|
|
448
|
+
console.warn("Token missing issued_at timestamp, treating as expired");
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
const expiresAt = tokens.issued_at + tokens.expires_in * 1e3;
|
|
443
452
|
const now = Date.now();
|
|
444
453
|
return expiresAt - now < 3e5;
|
|
445
454
|
}
|
|
446
|
-
getStoredAt(tokens) {
|
|
447
|
-
return Date.now() - 36e5;
|
|
448
|
-
}
|
|
449
455
|
async storeToFile(tokenString) {
|
|
450
456
|
if (!this.isNode()) return;
|
|
451
457
|
const fs = require("fs").promises;
|
|
@@ -1068,9 +1074,66 @@ var ApiKeyStorage = class {
|
|
|
1068
1074
|
}
|
|
1069
1075
|
};
|
|
1070
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
|
+
|
|
1071
1133
|
// src/client/mcp-client.ts
|
|
1072
1134
|
var MCPClient = class {
|
|
1073
1135
|
constructor(config = {}) {
|
|
1136
|
+
// ← NEW: Track auth mode
|
|
1074
1137
|
this.ws = null;
|
|
1075
1138
|
this.eventSource = null;
|
|
1076
1139
|
this.accessToken = null;
|
|
@@ -1081,30 +1144,45 @@ var MCPClient = class {
|
|
|
1081
1144
|
...config
|
|
1082
1145
|
};
|
|
1083
1146
|
this.tokenStorage = new TokenStorage();
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
+
);
|
|
1086
1153
|
} else {
|
|
1087
|
-
this.
|
|
1154
|
+
if (this.isTerminal()) {
|
|
1155
|
+
this.authFlow = new TerminalOAuthFlow(config);
|
|
1156
|
+
} else {
|
|
1157
|
+
this.authFlow = new DesktopOAuthFlow(config);
|
|
1158
|
+
}
|
|
1088
1159
|
}
|
|
1089
1160
|
}
|
|
1090
1161
|
async connect() {
|
|
1091
1162
|
try {
|
|
1092
1163
|
let tokens = await this.tokenStorage.retrieve();
|
|
1093
|
-
if (
|
|
1094
|
-
if (tokens
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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 {
|
|
1099
1179
|
tokens = await this.authenticate();
|
|
1100
1180
|
}
|
|
1101
|
-
} else {
|
|
1102
|
-
tokens = await this.authenticate();
|
|
1103
1181
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1182
|
+
this.accessToken = tokens.access_token;
|
|
1183
|
+
if (this.config.autoRefresh && tokens.expires_in) {
|
|
1184
|
+
this.scheduleTokenRefresh(tokens);
|
|
1185
|
+
}
|
|
1108
1186
|
}
|
|
1109
1187
|
await this.establishConnection();
|
|
1110
1188
|
} catch (error) {
|
|
@@ -1118,6 +1196,33 @@ var MCPClient = class {
|
|
|
1118
1196
|
await this.tokenStorage.store(tokens);
|
|
1119
1197
|
return tokens;
|
|
1120
1198
|
}
|
|
1199
|
+
async ensureAccessToken() {
|
|
1200
|
+
if (this.accessToken) return;
|
|
1201
|
+
const tokens = await this.tokenStorage.retrieve();
|
|
1202
|
+
if (!tokens) {
|
|
1203
|
+
throw new Error("Not authenticated");
|
|
1204
|
+
}
|
|
1205
|
+
if (this.authMode === "apikey") {
|
|
1206
|
+
this.accessToken = tokens.access_token;
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (this.tokenStorage.isTokenExpired(tokens)) {
|
|
1210
|
+
if (tokens.refresh_token) {
|
|
1211
|
+
try {
|
|
1212
|
+
const newTokens = await this.authFlow.refreshToken(tokens.refresh_token);
|
|
1213
|
+
await this.tokenStorage.store(newTokens);
|
|
1214
|
+
this.accessToken = newTokens.access_token;
|
|
1215
|
+
return;
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
console.error("Token refresh failed:", error);
|
|
1218
|
+
throw new Error("Token expired and refresh failed");
|
|
1219
|
+
}
|
|
1220
|
+
} else {
|
|
1221
|
+
throw new Error("Token expired and no refresh token available");
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
this.accessToken = tokens.access_token;
|
|
1225
|
+
}
|
|
1121
1226
|
scheduleTokenRefresh(tokens) {
|
|
1122
1227
|
if (this.refreshTimer) {
|
|
1123
1228
|
clearTimeout(this.refreshTimer);
|
|
@@ -1157,11 +1262,19 @@ var MCPClient = class {
|
|
|
1157
1262
|
this.ws = new WebSocket(wsUrl.toString());
|
|
1158
1263
|
} else {
|
|
1159
1264
|
const { default: WS } = await import("ws");
|
|
1160
|
-
this.
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
+
}
|
|
1165
1278
|
}
|
|
1166
1279
|
return new Promise((resolve, reject) => {
|
|
1167
1280
|
if (!this.ws) {
|
|
@@ -1195,11 +1308,19 @@ var MCPClient = class {
|
|
|
1195
1308
|
} else {
|
|
1196
1309
|
const EventSourceModule = await import("eventsource");
|
|
1197
1310
|
const ES = EventSourceModule.default || EventSourceModule;
|
|
1198
|
-
this.
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
+
}
|
|
1203
1324
|
}
|
|
1204
1325
|
this.eventSource.onopen = () => {
|
|
1205
1326
|
console.log("MCP SSE connected");
|
|
@@ -1225,15 +1346,21 @@ var MCPClient = class {
|
|
|
1225
1346
|
await this.establishConnection();
|
|
1226
1347
|
}
|
|
1227
1348
|
async request(method, params) {
|
|
1349
|
+
await this.ensureAccessToken();
|
|
1228
1350
|
if (!this.accessToken) {
|
|
1229
1351
|
throw new Error("Not authenticated");
|
|
1230
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
|
+
}
|
|
1231
1361
|
const response = await fetch(`${this.config.mcpEndpoint}/api`, {
|
|
1232
1362
|
method: "POST",
|
|
1233
|
-
headers
|
|
1234
|
-
"Authorization": `Bearer ${this.accessToken}`,
|
|
1235
|
-
"Content-Type": "application/json"
|
|
1236
|
-
},
|
|
1363
|
+
headers,
|
|
1237
1364
|
body: JSON.stringify({
|
|
1238
1365
|
jsonrpc: "2.0",
|
|
1239
1366
|
id: this.generateId(),
|
|
@@ -1242,6 +1369,9 @@ var MCPClient = class {
|
|
|
1242
1369
|
})
|
|
1243
1370
|
});
|
|
1244
1371
|
if (response.status === 401) {
|
|
1372
|
+
if (this.authMode === "apikey") {
|
|
1373
|
+
throw new Error("Invalid API key - please check your credentials");
|
|
1374
|
+
}
|
|
1245
1375
|
const tokens = await this.tokenStorage.retrieve();
|
|
1246
1376
|
if (tokens?.refresh_token) {
|
|
1247
1377
|
const newTokens = await this.authFlow.refreshToken(tokens.refresh_token);
|
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
interface TokenResponse {
|
|
2
2
|
access_token: string;
|
|
3
|
-
token_type: string;
|
|
4
|
-
expires_in: number;
|
|
5
3
|
refresh_token?: string;
|
|
6
|
-
|
|
4
|
+
expires_in: number;
|
|
5
|
+
token_type: string;
|
|
6
|
+
scope?: string;
|
|
7
|
+
issued_at?: number;
|
|
7
8
|
}
|
|
8
9
|
interface DeviceCodeResponse {
|
|
9
10
|
device_code: string;
|
|
@@ -77,8 +78,9 @@ declare class TokenStorage {
|
|
|
77
78
|
store(tokens: TokenResponse): Promise<void>;
|
|
78
79
|
retrieve(): Promise<TokenResponse | null>;
|
|
79
80
|
clear(): Promise<void>;
|
|
80
|
-
isTokenExpired(tokens: TokenResponse
|
|
81
|
-
|
|
81
|
+
isTokenExpired(tokens: TokenResponse & {
|
|
82
|
+
issued_at?: number;
|
|
83
|
+
}): boolean;
|
|
82
84
|
private storeToFile;
|
|
83
85
|
private retrieveFromFile;
|
|
84
86
|
private deleteFile;
|
|
@@ -170,11 +172,13 @@ declare class ApiKeyStorage {
|
|
|
170
172
|
interface MCPClientConfig extends Partial<OAuthConfig> {
|
|
171
173
|
mcpEndpoint?: string;
|
|
172
174
|
autoRefresh?: boolean;
|
|
175
|
+
apiKey?: string;
|
|
173
176
|
}
|
|
174
177
|
declare class MCPClient {
|
|
175
178
|
private tokenStorage;
|
|
176
179
|
private authFlow;
|
|
177
180
|
private config;
|
|
181
|
+
private authMode;
|
|
178
182
|
private ws;
|
|
179
183
|
private eventSource;
|
|
180
184
|
private accessToken;
|
|
@@ -182,6 +186,7 @@ declare class MCPClient {
|
|
|
182
186
|
constructor(config?: MCPClientConfig);
|
|
183
187
|
connect(): Promise<void>;
|
|
184
188
|
private authenticate;
|
|
189
|
+
private ensureAccessToken;
|
|
185
190
|
private scheduleTokenRefresh;
|
|
186
191
|
private establishConnection;
|
|
187
192
|
private connectWebSocket;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
interface TokenResponse {
|
|
2
2
|
access_token: string;
|
|
3
|
-
token_type: string;
|
|
4
|
-
expires_in: number;
|
|
5
3
|
refresh_token?: string;
|
|
6
|
-
|
|
4
|
+
expires_in: number;
|
|
5
|
+
token_type: string;
|
|
6
|
+
scope?: string;
|
|
7
|
+
issued_at?: number;
|
|
7
8
|
}
|
|
8
9
|
interface DeviceCodeResponse {
|
|
9
10
|
device_code: string;
|
|
@@ -77,8 +78,9 @@ declare class TokenStorage {
|
|
|
77
78
|
store(tokens: TokenResponse): Promise<void>;
|
|
78
79
|
retrieve(): Promise<TokenResponse | null>;
|
|
79
80
|
clear(): Promise<void>;
|
|
80
|
-
isTokenExpired(tokens: TokenResponse
|
|
81
|
-
|
|
81
|
+
isTokenExpired(tokens: TokenResponse & {
|
|
82
|
+
issued_at?: number;
|
|
83
|
+
}): boolean;
|
|
82
84
|
private storeToFile;
|
|
83
85
|
private retrieveFromFile;
|
|
84
86
|
private deleteFile;
|
|
@@ -170,11 +172,13 @@ declare class ApiKeyStorage {
|
|
|
170
172
|
interface MCPClientConfig extends Partial<OAuthConfig> {
|
|
171
173
|
mcpEndpoint?: string;
|
|
172
174
|
autoRefresh?: boolean;
|
|
175
|
+
apiKey?: string;
|
|
173
176
|
}
|
|
174
177
|
declare class MCPClient {
|
|
175
178
|
private tokenStorage;
|
|
176
179
|
private authFlow;
|
|
177
180
|
private config;
|
|
181
|
+
private authMode;
|
|
178
182
|
private ws;
|
|
179
183
|
private eventSource;
|
|
180
184
|
private accessToken;
|
|
@@ -182,6 +186,7 @@ declare class MCPClient {
|
|
|
182
186
|
constructor(config?: MCPClientConfig);
|
|
183
187
|
connect(): Promise<void>;
|
|
184
188
|
private authenticate;
|
|
189
|
+
private ensureAccessToken;
|
|
185
190
|
private scheduleTokenRefresh;
|
|
186
191
|
private establishConnection;
|
|
187
192
|
private connectWebSocket;
|
package/dist/index.js
CHANGED
|
@@ -344,7 +344,11 @@ var TokenStorage = class {
|
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
async store(tokens) {
|
|
347
|
-
const
|
|
347
|
+
const tokensWithTimestamp = {
|
|
348
|
+
...tokens,
|
|
349
|
+
issued_at: Date.now()
|
|
350
|
+
};
|
|
351
|
+
const tokenString = JSON.stringify(tokensWithTimestamp);
|
|
348
352
|
if (this.isNode()) {
|
|
349
353
|
if (this.keytar) {
|
|
350
354
|
await this.keytar.setPassword("lanonasis-mcp", "tokens", tokenString);
|
|
@@ -352,7 +356,7 @@ var TokenStorage = class {
|
|
|
352
356
|
await this.storeToFile(tokenString);
|
|
353
357
|
}
|
|
354
358
|
} else if (this.isElectron()) {
|
|
355
|
-
await window.electronAPI.secureStore.set(this.storageKey,
|
|
359
|
+
await window.electronAPI.secureStore.set(this.storageKey, tokensWithTimestamp);
|
|
356
360
|
} else if (this.isMobile()) {
|
|
357
361
|
await window.SecureStorage.set(this.storageKey, tokenString);
|
|
358
362
|
} else {
|
|
@@ -402,16 +406,18 @@ var TokenStorage = class {
|
|
|
402
406
|
}
|
|
403
407
|
}
|
|
404
408
|
isTokenExpired(tokens) {
|
|
409
|
+
if (tokens.token_type === "api-key" || tokens.expires_in === 0) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
405
412
|
if (!tokens.expires_in) return false;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
413
|
+
if (!tokens.issued_at) {
|
|
414
|
+
console.warn("Token missing issued_at timestamp, treating as expired");
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
const expiresAt = tokens.issued_at + tokens.expires_in * 1e3;
|
|
409
418
|
const now = Date.now();
|
|
410
419
|
return expiresAt - now < 3e5;
|
|
411
420
|
}
|
|
412
|
-
getStoredAt(tokens) {
|
|
413
|
-
return Date.now() - 36e5;
|
|
414
|
-
}
|
|
415
421
|
async storeToFile(tokenString) {
|
|
416
422
|
if (!this.isNode()) return;
|
|
417
423
|
const fs = __require("fs").promises;
|
|
@@ -1034,9 +1040,66 @@ var ApiKeyStorage = class {
|
|
|
1034
1040
|
}
|
|
1035
1041
|
};
|
|
1036
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
|
+
|
|
1037
1099
|
// src/client/mcp-client.ts
|
|
1038
1100
|
var MCPClient = class {
|
|
1039
1101
|
constructor(config = {}) {
|
|
1102
|
+
// ← NEW: Track auth mode
|
|
1040
1103
|
this.ws = null;
|
|
1041
1104
|
this.eventSource = null;
|
|
1042
1105
|
this.accessToken = null;
|
|
@@ -1047,30 +1110,45 @@ var MCPClient = class {
|
|
|
1047
1110
|
...config
|
|
1048
1111
|
};
|
|
1049
1112
|
this.tokenStorage = new TokenStorage();
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
+
);
|
|
1052
1119
|
} else {
|
|
1053
|
-
this.
|
|
1120
|
+
if (this.isTerminal()) {
|
|
1121
|
+
this.authFlow = new TerminalOAuthFlow(config);
|
|
1122
|
+
} else {
|
|
1123
|
+
this.authFlow = new DesktopOAuthFlow(config);
|
|
1124
|
+
}
|
|
1054
1125
|
}
|
|
1055
1126
|
}
|
|
1056
1127
|
async connect() {
|
|
1057
1128
|
try {
|
|
1058
1129
|
let tokens = await this.tokenStorage.retrieve();
|
|
1059
|
-
if (
|
|
1060
|
-
if (tokens
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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 {
|
|
1065
1145
|
tokens = await this.authenticate();
|
|
1066
1146
|
}
|
|
1067
|
-
} else {
|
|
1068
|
-
tokens = await this.authenticate();
|
|
1069
1147
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1148
|
+
this.accessToken = tokens.access_token;
|
|
1149
|
+
if (this.config.autoRefresh && tokens.expires_in) {
|
|
1150
|
+
this.scheduleTokenRefresh(tokens);
|
|
1151
|
+
}
|
|
1074
1152
|
}
|
|
1075
1153
|
await this.establishConnection();
|
|
1076
1154
|
} catch (error) {
|
|
@@ -1084,6 +1162,33 @@ var MCPClient = class {
|
|
|
1084
1162
|
await this.tokenStorage.store(tokens);
|
|
1085
1163
|
return tokens;
|
|
1086
1164
|
}
|
|
1165
|
+
async ensureAccessToken() {
|
|
1166
|
+
if (this.accessToken) return;
|
|
1167
|
+
const tokens = await this.tokenStorage.retrieve();
|
|
1168
|
+
if (!tokens) {
|
|
1169
|
+
throw new Error("Not authenticated");
|
|
1170
|
+
}
|
|
1171
|
+
if (this.authMode === "apikey") {
|
|
1172
|
+
this.accessToken = tokens.access_token;
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (this.tokenStorage.isTokenExpired(tokens)) {
|
|
1176
|
+
if (tokens.refresh_token) {
|
|
1177
|
+
try {
|
|
1178
|
+
const newTokens = await this.authFlow.refreshToken(tokens.refresh_token);
|
|
1179
|
+
await this.tokenStorage.store(newTokens);
|
|
1180
|
+
this.accessToken = newTokens.access_token;
|
|
1181
|
+
return;
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
console.error("Token refresh failed:", error);
|
|
1184
|
+
throw new Error("Token expired and refresh failed");
|
|
1185
|
+
}
|
|
1186
|
+
} else {
|
|
1187
|
+
throw new Error("Token expired and no refresh token available");
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
this.accessToken = tokens.access_token;
|
|
1191
|
+
}
|
|
1087
1192
|
scheduleTokenRefresh(tokens) {
|
|
1088
1193
|
if (this.refreshTimer) {
|
|
1089
1194
|
clearTimeout(this.refreshTimer);
|
|
@@ -1123,11 +1228,19 @@ var MCPClient = class {
|
|
|
1123
1228
|
this.ws = new WebSocket(wsUrl.toString());
|
|
1124
1229
|
} else {
|
|
1125
1230
|
const { default: WS } = await import("ws");
|
|
1126
|
-
this.
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
+
}
|
|
1131
1244
|
}
|
|
1132
1245
|
return new Promise((resolve, reject) => {
|
|
1133
1246
|
if (!this.ws) {
|
|
@@ -1161,11 +1274,19 @@ var MCPClient = class {
|
|
|
1161
1274
|
} else {
|
|
1162
1275
|
const EventSourceModule = await import("eventsource");
|
|
1163
1276
|
const ES = EventSourceModule.default || EventSourceModule;
|
|
1164
|
-
this.
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
+
}
|
|
1169
1290
|
}
|
|
1170
1291
|
this.eventSource.onopen = () => {
|
|
1171
1292
|
console.log("MCP SSE connected");
|
|
@@ -1191,15 +1312,21 @@ var MCPClient = class {
|
|
|
1191
1312
|
await this.establishConnection();
|
|
1192
1313
|
}
|
|
1193
1314
|
async request(method, params) {
|
|
1315
|
+
await this.ensureAccessToken();
|
|
1194
1316
|
if (!this.accessToken) {
|
|
1195
1317
|
throw new Error("Not authenticated");
|
|
1196
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
|
+
}
|
|
1197
1327
|
const response = await fetch(`${this.config.mcpEndpoint}/api`, {
|
|
1198
1328
|
method: "POST",
|
|
1199
|
-
headers
|
|
1200
|
-
"Authorization": `Bearer ${this.accessToken}`,
|
|
1201
|
-
"Content-Type": "application/json"
|
|
1202
|
-
},
|
|
1329
|
+
headers,
|
|
1203
1330
|
body: JSON.stringify({
|
|
1204
1331
|
jsonrpc: "2.0",
|
|
1205
1332
|
id: this.generateId(),
|
|
@@ -1208,6 +1335,9 @@ var MCPClient = class {
|
|
|
1208
1335
|
})
|
|
1209
1336
|
});
|
|
1210
1337
|
if (response.status === 401) {
|
|
1338
|
+
if (this.authMode === "apikey") {
|
|
1339
|
+
throw new Error("Invalid API key - please check your credentials");
|
|
1340
|
+
}
|
|
1211
1341
|
const tokens = await this.tokenStorage.retrieve();
|
|
1212
1342
|
if (tokens?.refresh_token) {
|
|
1213
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",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
18
|
-
"url": "https://github.com/lanonasis/v-secure.git",
|
|
18
|
+
"url": "git+https://github.com/lanonasis/v-secure.git",
|
|
19
19
|
"directory": "oauth-client"
|
|
20
20
|
},
|
|
21
21
|
"bugs": {
|
|
@@ -62,7 +62,6 @@
|
|
|
62
62
|
"@supabase/supabase-js": "^2.0.0"
|
|
63
63
|
},
|
|
64
64
|
"publishConfig": {
|
|
65
|
-
"access": "public"
|
|
66
|
-
"registry": "https://registry.npmjs.org/"
|
|
65
|
+
"access": "public"
|
|
67
66
|
}
|
|
68
67
|
}
|