@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 CHANGED
@@ -1,12 +1,15 @@
1
1
  # @lanonasis/oauth-client
2
2
 
3
- Drop-in OAuth + MCP connectivity client for the Lanonasis ecosystem. Handles browser/desktop/terminal flows, token lifecycle, secure (hashed) API key storage, and MCP WebSocket/SSE connections so other projects can integrate without re-implementing auth.
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(); // handles auth, refresh, and MCP 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 tokenString = JSON.stringify(tokens);
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, tokens);
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
- const storedAt = this.getStoredAt(tokens);
441
- if (!storedAt) return true;
442
- const expiresAt = storedAt + tokens.expires_in * 1e3;
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
- if (this.isTerminal()) {
1085
- this.authFlow = new TerminalOAuthFlow(config);
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.authFlow = new DesktopOAuthFlow(config);
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 (!tokens || this.tokenStorage.isTokenExpired(tokens)) {
1094
- if (tokens?.refresh_token) {
1095
- try {
1096
- tokens = await this.authFlow.refreshToken(tokens.refresh_token);
1097
- await this.tokenStorage.store(tokens);
1098
- } catch (error) {
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
- this.accessToken = tokens.access_token;
1106
- if (this.config.autoRefresh && tokens.expires_in) {
1107
- this.scheduleTokenRefresh(tokens);
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.ws = new WS(wsUrl.toString(), {
1161
- headers: {
1162
- "Authorization": `Bearer ${this.accessToken}`
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.eventSource = new ES(sseUrl.toString(), {
1199
- headers: {
1200
- "Authorization": `Bearer ${this.accessToken}`
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
- scope: string;
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): boolean;
81
- private getStoredAt;
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
- scope: string;
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): boolean;
81
- private getStoredAt;
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 tokenString = JSON.stringify(tokens);
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, tokens);
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
- const storedAt = this.getStoredAt(tokens);
407
- if (!storedAt) return true;
408
- const expiresAt = storedAt + tokens.expires_in * 1e3;
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
- if (this.isTerminal()) {
1051
- this.authFlow = new TerminalOAuthFlow(config);
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.authFlow = new DesktopOAuthFlow(config);
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 (!tokens || this.tokenStorage.isTokenExpired(tokens)) {
1060
- if (tokens?.refresh_token) {
1061
- try {
1062
- tokens = await this.authFlow.refreshToken(tokens.refresh_token);
1063
- await this.tokenStorage.store(tokens);
1064
- } catch (error) {
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
- this.accessToken = tokens.access_token;
1072
- if (this.config.autoRefresh && tokens.expires_in) {
1073
- this.scheduleTokenRefresh(tokens);
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.ws = new WS(wsUrl.toString(), {
1127
- headers: {
1128
- "Authorization": `Bearer ${this.accessToken}`
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.eventSource = new ES(sseUrl.toString(), {
1165
- headers: {
1166
- "Authorization": `Bearer ${this.accessToken}`
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.1",
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
  }