@lanonasis/oauth-client 2.0.0 → 2.0.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/README.md CHANGED
@@ -1,16 +1,32 @@
1
1
  # @lanonasis/oauth-client
2
2
 
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.
3
+ **THE single authentication package** for the Lanonasis ecosystem. Consolidates all auth patterns: OAuth2 PKCE, API keys, magic links, React hooks, and Express middleware.
4
+
5
+ > **v2.0**: Now includes `/react` and `/server` exports! Migrating from `@lanonasis/shared-auth`? See [Migration Guide](#migration-from-lanonasisshared-auth).
4
6
 
5
7
  ## Features
6
- - **Dual Authentication**: OAuth2 PKCE flow OR direct API key authentication
7
- - OAuth flows for terminal and desktop (Electron-friendly) environments
8
- - API key authentication for new users with dashboard-generated keys
9
- - Token storage with secure backends (Keytar, encrypted files, Electron secure store, mobile secure storage, WebCrypto in browsers); browser builds auto-use web storage
10
- - API key storage that normalizes to SHA-256 digests before persisting
11
- - MCP client that connects over WebSocket (`/ws`) or SSE (`/sse`) with auto-refreshing tokens
12
- - Automatic auth mode detection based on configuration
13
- - ESM + CJS bundles with a dedicated browser export to avoid Node-only deps
8
+
9
+ ### Core Authentication
10
+ - **OAuth2 PKCE Flow**: Terminal and desktop (Electron) environments
11
+ - **API Key Authentication**: Dashboard-generated keys for instant access
12
+ - **Magic Link / OTP Flow**: Passwordless email authentication
13
+ - **Token Storage**: Secure backends (Keytar, encrypted files, WebCrypto)
14
+
15
+ ### React Integration (`/react`)
16
+ - **`useSSO()`**: React hook for SSO authentication state
17
+ - **`useSSOSync()`**: Cross-subdomain session synchronization
18
+ - **Cookie utilities**: `parseUserCookie`, `hasSessionCookie`, `clearUserCookie`
19
+
20
+ ### Server Integration (`/server`)
21
+ - **`requireAuth()`**: Express middleware enforcing authentication
22
+ - **`optionalAuth()`**: Attach user if authenticated, allow anonymous
23
+ - **`requireRole(role)`**: Role-based access control middleware
24
+ - **Cookie parsing**: `getSSOUserFromRequest`, `getSessionTokenFromRequest`
25
+
26
+ ### Additional Features
27
+ - MCP client (WebSocket/SSE) with auto-refreshing tokens
28
+ - ESM + CJS bundles with dedicated browser export
29
+ - TypeScript types included
14
30
 
15
31
  ## Installation
16
32
  ```bash
@@ -76,6 +92,82 @@ const flow = new DesktopOAuthFlow({
76
92
  const tokens = await flow.authenticate();
77
93
  ```
78
94
 
95
+ ### Magic Link / OTP Flow (Passwordless)
96
+ ```ts
97
+ import { MagicLinkFlow } from '@lanonasis/oauth-client';
98
+
99
+ const flow = new MagicLinkFlow({
100
+ authBaseUrl: 'https://auth.lanonasis.com'
101
+ });
102
+
103
+ // Send OTP to user's email
104
+ await flow.sendOTP('user@example.com');
105
+
106
+ // User enters the code they received
107
+ const tokens = await flow.verifyOTP('user@example.com', '123456');
108
+ ```
109
+
110
+ ### React Hooks (`/react`)
111
+ ```tsx
112
+ import { useSSO, useSSOSync } from '@lanonasis/oauth-client/react';
113
+
114
+ function MyComponent() {
115
+ const { user, isAuthenticated, isLoading, error } = useSSO({
116
+ authGatewayUrl: 'https://auth.lanonasis.com',
117
+ projectScope: 'my-app'
118
+ });
119
+
120
+ if (isLoading) return <div>Loading...</div>;
121
+ if (!isAuthenticated) return <div>Please log in</div>;
122
+
123
+ return <div>Hello, {user.email}!</div>;
124
+ }
125
+
126
+ // For cross-subdomain session sync
127
+ function App() {
128
+ useSSOSync({ pollInterval: 30000 });
129
+ return <MyComponent />;
130
+ }
131
+ ```
132
+
133
+ ### Server Middleware (`/server`)
134
+ ```ts
135
+ import express from 'express';
136
+ import {
137
+ requireAuth,
138
+ optionalAuth,
139
+ requireRole,
140
+ getSSOUserFromRequest
141
+ } from '@lanonasis/oauth-client/server';
142
+
143
+ const app = express();
144
+
145
+ // Require authentication
146
+ app.get('/api/profile', requireAuth(), (req, res) => {
147
+ res.json({ user: req.user });
148
+ });
149
+
150
+ // Optional authentication (attach user if present)
151
+ app.get('/api/public', optionalAuth(), (req, res) => {
152
+ if (req.user) {
153
+ res.json({ message: `Hello ${req.user.email}` });
154
+ } else {
155
+ res.json({ message: 'Hello guest' });
156
+ }
157
+ });
158
+
159
+ // Role-based access control
160
+ app.delete('/api/admin/users/:id', requireAuth(), requireRole('admin'), (req, res) => {
161
+ // Only admins can reach here
162
+ });
163
+
164
+ // Manual user extraction
165
+ app.get('/api/check', (req, res) => {
166
+ const user = getSSOUserFromRequest(req);
167
+ res.json({ authenticated: !!user });
168
+ });
169
+ ```
170
+
79
171
  ### Token storage
80
172
  ```ts
81
173
  import { TokenStorage } from '@lanonasis/oauth-client';
@@ -85,15 +177,20 @@ await storage.store(tokens);
85
177
  const restored = await storage.retrieve();
86
178
  ```
87
179
 
88
- ### API key storage (hashes to SHA-256)
180
+ In Node and Electron environments, secure storage now lazy-loads `keytar` so the ESM bundle can still use the native OS keychain when available. If native keychain access is unavailable, storage falls back to the encrypted local file backend automatically.
181
+
182
+ ### API key storage
89
183
  ```ts
90
184
  import { ApiKeyStorage } from '@lanonasis/oauth-client';
91
185
 
92
186
  const apiKeys = new ApiKeyStorage();
93
187
  await apiKeys.store({ apiKey: 'lns_abc123', environment: 'production' });
94
- const hashed = await apiKeys.getApiKey(); // returns sha256 hex digest
188
+ const apiKey = await apiKeys.getApiKey(); // returns the original key for outbound auth headers
95
189
  ```
96
190
 
191
+ Node/Electron secure storage preserves the original secret value so clients can send it back
192
+ in `X-API-Key` or `Authorization` headers when talking to remote services.
193
+
97
194
  ## Configuration
98
195
 
99
196
  ### API Key Mode
@@ -116,10 +213,11 @@ const hashed = await apiKeys.getApiKey(); // returns sha256 hex digest
116
213
  - The terminal/device code flow remains Node-only; in browser previews use API key or desktop flow.
117
214
 
118
215
  ## Publishing (maintainers)
119
- 1) Build artifacts: `npm install && npm run build`
120
- 2) Verify contents: ensure `dist`, `README.md`, `LICENSE` are present.
121
- 3) Publish: `npm publish --access public` (registry must have 2FA as configured).
122
- 4) Tag in git (optional): `git tag oauth-client-v1.0.0 && git push --tags`
216
+ 1) Install and verify: `npm install && npm test && npm run build`
217
+ 2) Audit release deps: `bun audit`
218
+ 3) Verify contents: ensure `dist`, `README.md`, and `LICENSE` are present.
219
+ 4) Publish: `npm publish --access public` (registry must have 2FA as configured).
220
+ 5) Tag in git (optional): `git tag oauth-client-vX.Y.Z && git push --tags`
123
221
 
124
222
  ## Files shipped
125
223
  - `dist/*` compiled CJS/ESM bundles + types
@@ -145,5 +243,56 @@ This SDK implements client-side equivalents of the auth-gateway's authentication
145
243
 
146
244
  See [auth-gateway/AUTHENTICATION-METHODS.md](../../apps/onasis-core/services/auth-gateway/AUTHENTICATION-METHODS.md) for complete server-side documentation.
147
245
 
246
+ ## Exports
247
+
248
+ | Export | Description | Use Case |
249
+ |--------|-------------|----------|
250
+ | `@lanonasis/oauth-client` | Main entry - OAuth flows, MCP client, storage | Node.js CLI, servers |
251
+ | `@lanonasis/oauth-client/browser` | Browser-safe bundle (no Node deps) | Web apps, SPAs |
252
+ | `@lanonasis/oauth-client/react` | React hooks + browser cookie utils | React/Next.js apps |
253
+ | `@lanonasis/oauth-client/server` | Express middleware + server cookie utils | Express/Node servers |
254
+
255
+ ## Migration from @lanonasis/shared-auth
256
+
257
+ `@lanonasis/shared-auth` is now **deprecated**. All functionality has been consolidated into `@lanonasis/oauth-client`.
258
+
259
+ ### Import Changes
260
+
261
+ ```typescript
262
+ // ❌ BEFORE (deprecated)
263
+ import { useSSO, useSSOSync } from '@lanonasis/shared-auth';
264
+ import { getSSOUserFromRequest } from '@lanonasis/shared-auth/server';
265
+
266
+ // ✅ AFTER (recommended)
267
+ import { useSSO, useSSOSync } from '@lanonasis/oauth-client/react';
268
+ import { getSSOUserFromRequest, requireAuth } from '@lanonasis/oauth-client/server';
269
+ ```
270
+
271
+ ### Package.json Changes
272
+
273
+ ```json
274
+ {
275
+ "dependencies": {
276
+ // ❌ Remove
277
+ "@lanonasis/shared-auth": "^1.x.x",
278
+
279
+ // ✅ Add/Update
280
+ "@lanonasis/oauth-client": "^2.0.0"
281
+ }
282
+ }
283
+ ```
284
+
285
+ ### New Middleware (Bonus!)
286
+
287
+ The migration gives you access to new middleware that wasn't in shared-auth:
288
+
289
+ ```ts
290
+ import { requireAuth, optionalAuth, requireRole } from '@lanonasis/oauth-client/server';
291
+
292
+ // These are new! Replace manual auth checks
293
+ app.get('/api/protected', requireAuth(), handler);
294
+ app.delete('/api/admin/*', requireAuth(), requireRole('admin'), handler);
295
+ ```
296
+
148
297
  ## License
149
298
  MIT © Lan Onasis
@@ -106,7 +106,8 @@ declare class TokenStorage implements TokenStorageAdapter {
106
106
  private readonly storageKey;
107
107
  private readonly webEncryptionKeyStorage;
108
108
  private keytar;
109
- constructor();
109
+ private keytarLoadAttempted;
110
+ private getKeytar;
110
111
  store(tokens: TokenResponse): Promise<void>;
111
112
  retrieve(): Promise<TokenResponse | null>;
112
113
  clear(): Promise<void>;
@@ -182,8 +183,9 @@ declare class ApiKeyStorage {
182
183
  private readonly legacyConfigKey;
183
184
  private readonly webEncryptionKeyStorage;
184
185
  private keytar;
186
+ private keytarLoadAttempted;
185
187
  private migrationCompleted;
186
- constructor();
188
+ private getKeytar;
187
189
  /**
188
190
  * Initialize and migrate from legacy storage if needed
189
191
  */
@@ -235,8 +237,12 @@ declare class ApiKeyStorage {
235
237
  private base64Encode;
236
238
  private base64Decode;
237
239
  /**
238
- * Normalize API keys to a SHA-256 hex digest.
239
- * Accepts pre-hashed input and lowercases it to prevent double hashing.
240
+ * Normalize stored credentials without transforming their value.
241
+ *
242
+ * These secrets are stored in encrypted local storage/keychains and must remain
243
+ * usable for outbound authentication headers (for example X-API-Key or Bearer).
244
+ * Hashing here would make the original credential unrecoverable and break
245
+ * clients that need to send the raw key/token back to a remote service.
240
246
  */
241
247
  private normalizeApiKey;
242
248
  }
@@ -257,4 +263,4 @@ declare class ApiKeyStorageWeb {
257
263
  private base64Decode;
258
264
  }
259
265
 
260
- export { AuthGatewayClient as A, BaseOAuthFlow as B, DesktopOAuthFlow as D, type GrantType as G, type OAuthConfig as O, type PKCEChallenge as P, type TokenStorageAdapter as T, TokenStorageWeb as a, ApiKeyStorageWeb as b, type TokenResponse as c, type DeviceCodeResponse as d, type AuthError as e, type AuthTokenType as f, type AuthValidationResult as g, type AuthGatewayClientConfig as h, type TokenExchangeOptions as i, type TokenExchangeResponse as j, TokenStorage as k, type ApiKeyData as l, ApiKeyStorage as m };
266
+ export { ApiKeyStorageWeb as A, BaseOAuthFlow as B, DesktopOAuthFlow as D, type GrantType as G, type OAuthConfig as O, type PKCEChallenge as P, type TokenStorageAdapter as T, type AuthError as a, AuthGatewayClient as b, type AuthGatewayClientConfig as c, type AuthTokenType as d, type AuthValidationResult as e, type DeviceCodeResponse as f, type TokenExchangeOptions as g, type TokenExchangeResponse as h, type TokenResponse as i, TokenStorageWeb as j, type ApiKeyData as k, ApiKeyStorage as l, TokenStorage as m };
@@ -106,7 +106,8 @@ declare class TokenStorage implements TokenStorageAdapter {
106
106
  private readonly storageKey;
107
107
  private readonly webEncryptionKeyStorage;
108
108
  private keytar;
109
- constructor();
109
+ private keytarLoadAttempted;
110
+ private getKeytar;
110
111
  store(tokens: TokenResponse): Promise<void>;
111
112
  retrieve(): Promise<TokenResponse | null>;
112
113
  clear(): Promise<void>;
@@ -182,8 +183,9 @@ declare class ApiKeyStorage {
182
183
  private readonly legacyConfigKey;
183
184
  private readonly webEncryptionKeyStorage;
184
185
  private keytar;
186
+ private keytarLoadAttempted;
185
187
  private migrationCompleted;
186
- constructor();
188
+ private getKeytar;
187
189
  /**
188
190
  * Initialize and migrate from legacy storage if needed
189
191
  */
@@ -235,8 +237,12 @@ declare class ApiKeyStorage {
235
237
  private base64Encode;
236
238
  private base64Decode;
237
239
  /**
238
- * Normalize API keys to a SHA-256 hex digest.
239
- * Accepts pre-hashed input and lowercases it to prevent double hashing.
240
+ * Normalize stored credentials without transforming their value.
241
+ *
242
+ * These secrets are stored in encrypted local storage/keychains and must remain
243
+ * usable for outbound authentication headers (for example X-API-Key or Bearer).
244
+ * Hashing here would make the original credential unrecoverable and break
245
+ * clients that need to send the raw key/token back to a remote service.
240
246
  */
241
247
  private normalizeApiKey;
242
248
  }
@@ -257,4 +263,4 @@ declare class ApiKeyStorageWeb {
257
263
  private base64Decode;
258
264
  }
259
265
 
260
- export { AuthGatewayClient as A, BaseOAuthFlow as B, DesktopOAuthFlow as D, type GrantType as G, type OAuthConfig as O, type PKCEChallenge as P, type TokenStorageAdapter as T, TokenStorageWeb as a, ApiKeyStorageWeb as b, type TokenResponse as c, type DeviceCodeResponse as d, type AuthError as e, type AuthTokenType as f, type AuthValidationResult as g, type AuthGatewayClientConfig as h, type TokenExchangeOptions as i, type TokenExchangeResponse as j, TokenStorage as k, type ApiKeyData as l, ApiKeyStorage as m };
266
+ export { ApiKeyStorageWeb as A, BaseOAuthFlow as B, DesktopOAuthFlow as D, type GrantType as G, type OAuthConfig as O, type PKCEChallenge as P, type TokenStorageAdapter as T, type AuthError as a, AuthGatewayClient as b, type AuthGatewayClientConfig as c, type AuthTokenType as d, type AuthValidationResult as e, type DeviceCodeResponse as f, type TokenExchangeOptions as g, type TokenExchangeResponse as h, type TokenResponse as i, TokenStorageWeb as j, type ApiKeyData as k, ApiKeyStorage as l, TokenStorage as m };
@@ -1,5 +1,5 @@
1
- import { O as OAuthConfig, T as TokenStorageAdapter } from './api-key-storage-web-J3W8nQi2.cjs';
2
- export { b as ApiKeyStorageWeb, e as AuthError, A as AuthGatewayClient, h as AuthGatewayClientConfig, f as AuthTokenType, g as AuthValidationResult, B as BaseOAuthFlow, D as DesktopOAuthFlow, d as DeviceCodeResponse, G as GrantType, P as PKCEChallenge, i as TokenExchangeOptions, j as TokenExchangeResponse, c as TokenResponse, a as TokenStorageWeb } from './api-key-storage-web-J3W8nQi2.cjs';
1
+ import { O as OAuthConfig, T as TokenStorageAdapter } from './api-key-storage-web-DUyiN9mC.cjs';
2
+ export { A as ApiKeyStorageWeb, a as AuthError, b as AuthGatewayClient, c as AuthGatewayClientConfig, d as AuthTokenType, e as AuthValidationResult, B as BaseOAuthFlow, D as DesktopOAuthFlow, f as DeviceCodeResponse, G as GrantType, P as PKCEChallenge, g as TokenExchangeOptions, h as TokenExchangeResponse, i as TokenResponse, j as TokenStorageWeb } from './api-key-storage-web-DUyiN9mC.cjs';
3
3
 
4
4
  interface MCPClientConfig extends Partial<OAuthConfig> {
5
5
  mcpEndpoint?: string;
package/dist/browser.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { O as OAuthConfig, T as TokenStorageAdapter } from './api-key-storage-web-J3W8nQi2.js';
2
- export { b as ApiKeyStorageWeb, e as AuthError, A as AuthGatewayClient, h as AuthGatewayClientConfig, f as AuthTokenType, g as AuthValidationResult, B as BaseOAuthFlow, D as DesktopOAuthFlow, d as DeviceCodeResponse, G as GrantType, P as PKCEChallenge, i as TokenExchangeOptions, j as TokenExchangeResponse, c as TokenResponse, a as TokenStorageWeb } from './api-key-storage-web-J3W8nQi2.js';
1
+ import { O as OAuthConfig, T as TokenStorageAdapter } from './api-key-storage-web-DUyiN9mC.js';
2
+ export { A as ApiKeyStorageWeb, a as AuthError, b as AuthGatewayClient, c as AuthGatewayClientConfig, d as AuthTokenType, e as AuthValidationResult, B as BaseOAuthFlow, D as DesktopOAuthFlow, f as DeviceCodeResponse, G as GrantType, P as PKCEChallenge, g as TokenExchangeOptions, h as TokenExchangeResponse, i as TokenResponse, j as TokenStorageWeb } from './api-key-storage-web-DUyiN9mC.js';
3
3
 
4
4
  interface MCPClientConfig extends Partial<OAuthConfig> {
5
5
  mcpEndpoint?: string;
@@ -107,4 +107,4 @@ declare const DEFAULT_POLL_INTERVAL = 30000;
107
107
  */
108
108
  declare const DEFAULT_PROJECT_SCOPE = "lanonasis-maas";
109
109
 
110
- export { COOKIE_NAMES as C, DEFAULT_AUTH_GATEWAY as D, type SSOConfig as S, type UseSSOReturn as U, type SupabaseSession as a, type SSOSyncConfig as b, type UseSSOSyncReturn as c, type SSOUser as d, type SSOState as e, DEFAULT_COOKIE_DOMAIN as f, DEFAULT_POLL_INTERVAL as g, DEFAULT_PROJECT_SCOPE as h };
110
+ export { COOKIE_NAMES as C, DEFAULT_AUTH_GATEWAY as D, type SSOConfig as S, type UseSSOReturn as U, type SupabaseSession as a, type SSOSyncConfig as b, type UseSSOSyncReturn as c, type SSOUser as d, DEFAULT_COOKIE_DOMAIN as e, DEFAULT_POLL_INTERVAL as f, DEFAULT_PROJECT_SCOPE as g, type SSOState as h };
@@ -107,4 +107,4 @@ declare const DEFAULT_POLL_INTERVAL = 30000;
107
107
  */
108
108
  declare const DEFAULT_PROJECT_SCOPE = "lanonasis-maas";
109
109
 
110
- export { COOKIE_NAMES as C, DEFAULT_AUTH_GATEWAY as D, type SSOConfig as S, type UseSSOReturn as U, type SupabaseSession as a, type SSOSyncConfig as b, type UseSSOSyncReturn as c, type SSOUser as d, type SSOState as e, DEFAULT_COOKIE_DOMAIN as f, DEFAULT_POLL_INTERVAL as g, DEFAULT_PROJECT_SCOPE as h };
110
+ export { COOKIE_NAMES as C, DEFAULT_AUTH_GATEWAY as D, type SSOConfig as S, type UseSSOReturn as U, type SupabaseSession as a, type SSOSyncConfig as b, type UseSSOSyncReturn as c, type SSOUser as d, DEFAULT_COOKIE_DOMAIN as e, DEFAULT_POLL_INTERVAL as f, DEFAULT_PROJECT_SCOPE as g, type SSOState as h };
package/dist/index.cjs CHANGED
@@ -661,13 +661,25 @@ var TokenStorage = class {
661
661
  constructor() {
662
662
  this.storageKey = "lanonasis_mcp_tokens";
663
663
  this.webEncryptionKeyStorage = "lanonasis_web_token_enc_key";
664
- if (this.isNode()) {
665
- try {
666
- this.keytar = require("keytar");
667
- } catch (e) {
668
- console.warn("Keytar not available - falling back to file storage");
669
- }
664
+ this.keytar = null;
665
+ this.keytarLoadAttempted = false;
666
+ }
667
+ async getKeytar() {
668
+ if (!this.isNode()) {
669
+ return null;
670
670
  }
671
+ if (this.keytarLoadAttempted) {
672
+ return this.keytar;
673
+ }
674
+ this.keytarLoadAttempted = true;
675
+ try {
676
+ const keytarModule = await import("keytar");
677
+ this.keytar = keytarModule.default ?? keytarModule;
678
+ } catch {
679
+ this.keytar = null;
680
+ console.warn("Keytar not available - falling back to file storage");
681
+ }
682
+ return this.keytar;
671
683
  }
672
684
  async store(tokens) {
673
685
  const tokensWithTimestamp = {
@@ -676,8 +688,9 @@ var TokenStorage = class {
676
688
  };
677
689
  const tokenString = JSON.stringify(tokensWithTimestamp);
678
690
  if (this.isNode()) {
679
- if (this.keytar) {
680
- await this.keytar.setPassword("lanonasis-mcp", "tokens", tokenString);
691
+ const keytar = await this.getKeytar();
692
+ if (keytar) {
693
+ await keytar.setPassword("lanonasis-mcp", "tokens", tokenString);
681
694
  } else {
682
695
  await this.storeToFile(tokenString);
683
696
  }
@@ -694,8 +707,9 @@ var TokenStorage = class {
694
707
  let tokenString = null;
695
708
  try {
696
709
  if (this.isNode()) {
697
- if (this.keytar) {
698
- tokenString = await this.keytar.getPassword("lanonasis-mcp", "tokens");
710
+ const keytar = await this.getKeytar();
711
+ if (keytar) {
712
+ tokenString = await keytar.getPassword("lanonasis-mcp", "tokens");
699
713
  }
700
714
  if (!tokenString) {
701
715
  tokenString = await this.retrieveFromFile();
@@ -719,8 +733,9 @@ var TokenStorage = class {
719
733
  }
720
734
  async clear() {
721
735
  if (this.isNode()) {
722
- if (this.keytar) {
723
- await this.keytar.deletePassword("lanonasis-mcp", "tokens");
736
+ const keytar = await this.getKeytar();
737
+ if (keytar) {
738
+ await keytar.deletePassword("lanonasis-mcp", "tokens");
724
739
  }
725
740
  await this.deleteFile();
726
741
  } else if (this.isElectron()) {
@@ -965,14 +980,26 @@ var ApiKeyStorage = class {
965
980
  this.storageKey = "lanonasis_api_key";
966
981
  this.legacyConfigKey = "lanonasis_legacy_api_key";
967
982
  this.webEncryptionKeyStorage = "lanonasis_web_enc_key";
983
+ this.keytar = null;
984
+ this.keytarLoadAttempted = false;
968
985
  this.migrationCompleted = false;
969
- if (this.isNode()) {
970
- try {
971
- this.keytar = require("keytar");
972
- } catch (e) {
973
- console.warn("Keytar not available - falling back to encrypted file storage");
974
- }
986
+ }
987
+ async getKeytar() {
988
+ if (!this.isNode()) {
989
+ return null;
990
+ }
991
+ if (this.keytarLoadAttempted) {
992
+ return this.keytar;
993
+ }
994
+ this.keytarLoadAttempted = true;
995
+ try {
996
+ const keytarModule = await import("keytar");
997
+ this.keytar = keytarModule.default ?? keytarModule;
998
+ } catch {
999
+ this.keytar = null;
1000
+ console.warn("Keytar not available - falling back to encrypted file storage");
975
1001
  }
1002
+ return this.keytar;
976
1003
  }
977
1004
  /**
978
1005
  * Initialize and migrate from legacy storage if needed
@@ -992,9 +1019,10 @@ var ApiKeyStorage = class {
992
1019
  };
993
1020
  const keyString = JSON.stringify(dataWithTimestamp);
994
1021
  if (this.isNode()) {
995
- if (this.keytar) {
1022
+ const keytar = await this.getKeytar();
1023
+ if (keytar) {
996
1024
  try {
997
- await this.keytar.setPassword("lanonasis-mcp", this.storageKey, keyString);
1025
+ await keytar.setPassword("lanonasis-mcp", this.storageKey, keyString);
998
1026
  return;
999
1027
  } catch (error) {
1000
1028
  console.warn("Keytar storage failed, falling back to file:", error);
@@ -1017,9 +1045,10 @@ var ApiKeyStorage = class {
1017
1045
  let keyString = null;
1018
1046
  try {
1019
1047
  if (this.isNode()) {
1020
- if (this.keytar) {
1048
+ const keytar = await this.getKeytar();
1049
+ if (keytar) {
1021
1050
  try {
1022
- keyString = await this.keytar.getPassword("lanonasis-mcp", this.storageKey);
1051
+ keyString = await keytar.getPassword("lanonasis-mcp", this.storageKey);
1023
1052
  } catch (error) {
1024
1053
  console.warn("Keytar retrieval failed, trying file:", error);
1025
1054
  }
@@ -1077,9 +1106,10 @@ var ApiKeyStorage = class {
1077
1106
  */
1078
1107
  async clear() {
1079
1108
  if (this.isNode()) {
1080
- if (this.keytar) {
1109
+ const keytar = await this.getKeytar();
1110
+ if (keytar) {
1081
1111
  try {
1082
- await this.keytar.deletePassword("lanonasis-mcp", this.storageKey);
1112
+ await keytar.deletePassword("lanonasis-mcp", this.storageKey);
1083
1113
  } catch (error) {
1084
1114
  console.warn("Keytar deletion failed:", error);
1085
1115
  }
@@ -1381,26 +1411,19 @@ var ApiKeyStorage = class {
1381
1411
  throw new Error("No base64 decoder available");
1382
1412
  }
1383
1413
  /**
1384
- * Normalize API keys to a SHA-256 hex digest.
1385
- * Accepts pre-hashed input and lowercases it to prevent double hashing.
1414
+ * Normalize stored credentials without transforming their value.
1415
+ *
1416
+ * These secrets are stored in encrypted local storage/keychains and must remain
1417
+ * usable for outbound authentication headers (for example X-API-Key or Bearer).
1418
+ * Hashing here would make the original credential unrecoverable and break
1419
+ * clients that need to send the raw key/token back to a remote service.
1386
1420
  */
1387
1421
  async normalizeApiKey(apiKey) {
1388
1422
  const value = apiKey?.trim();
1389
1423
  if (!value) {
1390
1424
  throw new Error("API key must be a non-empty string");
1391
1425
  }
1392
- if (/^[a-f0-9]{64}$/i.test(value)) {
1393
- return value.toLowerCase();
1394
- }
1395
- if (typeof globalThis !== "undefined" && globalThis.crypto?.subtle) {
1396
- const encoder = new TextEncoder();
1397
- const data = encoder.encode(value);
1398
- const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", data);
1399
- const hashArray = Array.from(new Uint8Array(hashBuffer));
1400
- return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
1401
- }
1402
- const nodeCrypto = await import("crypto");
1403
- return nodeCrypto.createHash("sha256").update(value).digest("hex");
1426
+ return value;
1404
1427
  }
1405
1428
  };
1406
1429
 
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { B as BaseOAuthFlow, O as OAuthConfig, c as TokenResponse, T as TokenStorageAdapter } from './api-key-storage-web-J3W8nQi2.cjs';
2
- export { l as ApiKeyData, m as ApiKeyStorage, b as ApiKeyStorageWeb, e as AuthError, A as AuthGatewayClient, h as AuthGatewayClientConfig, f as AuthTokenType, g as AuthValidationResult, D as DesktopOAuthFlow, d as DeviceCodeResponse, G as GrantType, P as PKCEChallenge, i as TokenExchangeOptions, j as TokenExchangeResponse, k as TokenStorage, a as TokenStorageWeb } from './api-key-storage-web-J3W8nQi2.cjs';
1
+ import { B as BaseOAuthFlow, O as OAuthConfig, i as TokenResponse, T as TokenStorageAdapter } from './api-key-storage-web-DUyiN9mC.cjs';
2
+ export { k as ApiKeyData, l as ApiKeyStorage, A as ApiKeyStorageWeb, a as AuthError, b as AuthGatewayClient, c as AuthGatewayClientConfig, d as AuthTokenType, e as AuthValidationResult, D as DesktopOAuthFlow, f as DeviceCodeResponse, G as GrantType, P as PKCEChallenge, g as TokenExchangeOptions, h as TokenExchangeResponse, m as TokenStorage, j as TokenStorageWeb } from './api-key-storage-web-DUyiN9mC.cjs';
3
3
 
4
4
  declare class TerminalOAuthFlow extends BaseOAuthFlow {
5
5
  private pollInterval;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { B as BaseOAuthFlow, O as OAuthConfig, c as TokenResponse, T as TokenStorageAdapter } from './api-key-storage-web-J3W8nQi2.js';
2
- export { l as ApiKeyData, m as ApiKeyStorage, b as ApiKeyStorageWeb, e as AuthError, A as AuthGatewayClient, h as AuthGatewayClientConfig, f as AuthTokenType, g as AuthValidationResult, D as DesktopOAuthFlow, d as DeviceCodeResponse, G as GrantType, P as PKCEChallenge, i as TokenExchangeOptions, j as TokenExchangeResponse, k as TokenStorage, a as TokenStorageWeb } from './api-key-storage-web-J3W8nQi2.js';
1
+ import { B as BaseOAuthFlow, O as OAuthConfig, i as TokenResponse, T as TokenStorageAdapter } from './api-key-storage-web-DUyiN9mC.js';
2
+ export { k as ApiKeyData, l as ApiKeyStorage, A as ApiKeyStorageWeb, a as AuthError, b as AuthGatewayClient, c as AuthGatewayClientConfig, d as AuthTokenType, e as AuthValidationResult, D as DesktopOAuthFlow, f as DeviceCodeResponse, G as GrantType, P as PKCEChallenge, g as TokenExchangeOptions, h as TokenExchangeResponse, m as TokenStorage, j as TokenStorageWeb } from './api-key-storage-web-DUyiN9mC.js';
3
3
 
4
4
  declare class TerminalOAuthFlow extends BaseOAuthFlow {
5
5
  private pollInterval;
package/dist/index.mjs CHANGED
@@ -622,13 +622,25 @@ var TokenStorage = class {
622
622
  constructor() {
623
623
  this.storageKey = "lanonasis_mcp_tokens";
624
624
  this.webEncryptionKeyStorage = "lanonasis_web_token_enc_key";
625
- if (this.isNode()) {
626
- try {
627
- this.keytar = __require("keytar");
628
- } catch (e) {
629
- console.warn("Keytar not available - falling back to file storage");
630
- }
625
+ this.keytar = null;
626
+ this.keytarLoadAttempted = false;
627
+ }
628
+ async getKeytar() {
629
+ if (!this.isNode()) {
630
+ return null;
631
631
  }
632
+ if (this.keytarLoadAttempted) {
633
+ return this.keytar;
634
+ }
635
+ this.keytarLoadAttempted = true;
636
+ try {
637
+ const keytarModule = await import("keytar");
638
+ this.keytar = keytarModule.default ?? keytarModule;
639
+ } catch {
640
+ this.keytar = null;
641
+ console.warn("Keytar not available - falling back to file storage");
642
+ }
643
+ return this.keytar;
632
644
  }
633
645
  async store(tokens) {
634
646
  const tokensWithTimestamp = {
@@ -637,8 +649,9 @@ var TokenStorage = class {
637
649
  };
638
650
  const tokenString = JSON.stringify(tokensWithTimestamp);
639
651
  if (this.isNode()) {
640
- if (this.keytar) {
641
- await this.keytar.setPassword("lanonasis-mcp", "tokens", tokenString);
652
+ const keytar = await this.getKeytar();
653
+ if (keytar) {
654
+ await keytar.setPassword("lanonasis-mcp", "tokens", tokenString);
642
655
  } else {
643
656
  await this.storeToFile(tokenString);
644
657
  }
@@ -655,8 +668,9 @@ var TokenStorage = class {
655
668
  let tokenString = null;
656
669
  try {
657
670
  if (this.isNode()) {
658
- if (this.keytar) {
659
- tokenString = await this.keytar.getPassword("lanonasis-mcp", "tokens");
671
+ const keytar = await this.getKeytar();
672
+ if (keytar) {
673
+ tokenString = await keytar.getPassword("lanonasis-mcp", "tokens");
660
674
  }
661
675
  if (!tokenString) {
662
676
  tokenString = await this.retrieveFromFile();
@@ -680,8 +694,9 @@ var TokenStorage = class {
680
694
  }
681
695
  async clear() {
682
696
  if (this.isNode()) {
683
- if (this.keytar) {
684
- await this.keytar.deletePassword("lanonasis-mcp", "tokens");
697
+ const keytar = await this.getKeytar();
698
+ if (keytar) {
699
+ await keytar.deletePassword("lanonasis-mcp", "tokens");
685
700
  }
686
701
  await this.deleteFile();
687
702
  } else if (this.isElectron()) {
@@ -926,14 +941,26 @@ var ApiKeyStorage = class {
926
941
  this.storageKey = "lanonasis_api_key";
927
942
  this.legacyConfigKey = "lanonasis_legacy_api_key";
928
943
  this.webEncryptionKeyStorage = "lanonasis_web_enc_key";
944
+ this.keytar = null;
945
+ this.keytarLoadAttempted = false;
929
946
  this.migrationCompleted = false;
930
- if (this.isNode()) {
931
- try {
932
- this.keytar = __require("keytar");
933
- } catch (e) {
934
- console.warn("Keytar not available - falling back to encrypted file storage");
935
- }
947
+ }
948
+ async getKeytar() {
949
+ if (!this.isNode()) {
950
+ return null;
951
+ }
952
+ if (this.keytarLoadAttempted) {
953
+ return this.keytar;
954
+ }
955
+ this.keytarLoadAttempted = true;
956
+ try {
957
+ const keytarModule = await import("keytar");
958
+ this.keytar = keytarModule.default ?? keytarModule;
959
+ } catch {
960
+ this.keytar = null;
961
+ console.warn("Keytar not available - falling back to encrypted file storage");
936
962
  }
963
+ return this.keytar;
937
964
  }
938
965
  /**
939
966
  * Initialize and migrate from legacy storage if needed
@@ -953,9 +980,10 @@ var ApiKeyStorage = class {
953
980
  };
954
981
  const keyString = JSON.stringify(dataWithTimestamp);
955
982
  if (this.isNode()) {
956
- if (this.keytar) {
983
+ const keytar = await this.getKeytar();
984
+ if (keytar) {
957
985
  try {
958
- await this.keytar.setPassword("lanonasis-mcp", this.storageKey, keyString);
986
+ await keytar.setPassword("lanonasis-mcp", this.storageKey, keyString);
959
987
  return;
960
988
  } catch (error) {
961
989
  console.warn("Keytar storage failed, falling back to file:", error);
@@ -978,9 +1006,10 @@ var ApiKeyStorage = class {
978
1006
  let keyString = null;
979
1007
  try {
980
1008
  if (this.isNode()) {
981
- if (this.keytar) {
1009
+ const keytar = await this.getKeytar();
1010
+ if (keytar) {
982
1011
  try {
983
- keyString = await this.keytar.getPassword("lanonasis-mcp", this.storageKey);
1012
+ keyString = await keytar.getPassword("lanonasis-mcp", this.storageKey);
984
1013
  } catch (error) {
985
1014
  console.warn("Keytar retrieval failed, trying file:", error);
986
1015
  }
@@ -1038,9 +1067,10 @@ var ApiKeyStorage = class {
1038
1067
  */
1039
1068
  async clear() {
1040
1069
  if (this.isNode()) {
1041
- if (this.keytar) {
1070
+ const keytar = await this.getKeytar();
1071
+ if (keytar) {
1042
1072
  try {
1043
- await this.keytar.deletePassword("lanonasis-mcp", this.storageKey);
1073
+ await keytar.deletePassword("lanonasis-mcp", this.storageKey);
1044
1074
  } catch (error) {
1045
1075
  console.warn("Keytar deletion failed:", error);
1046
1076
  }
@@ -1342,26 +1372,19 @@ var ApiKeyStorage = class {
1342
1372
  throw new Error("No base64 decoder available");
1343
1373
  }
1344
1374
  /**
1345
- * Normalize API keys to a SHA-256 hex digest.
1346
- * Accepts pre-hashed input and lowercases it to prevent double hashing.
1375
+ * Normalize stored credentials without transforming their value.
1376
+ *
1377
+ * These secrets are stored in encrypted local storage/keychains and must remain
1378
+ * usable for outbound authentication headers (for example X-API-Key or Bearer).
1379
+ * Hashing here would make the original credential unrecoverable and break
1380
+ * clients that need to send the raw key/token back to a remote service.
1347
1381
  */
1348
1382
  async normalizeApiKey(apiKey) {
1349
1383
  const value = apiKey?.trim();
1350
1384
  if (!value) {
1351
1385
  throw new Error("API key must be a non-empty string");
1352
1386
  }
1353
- if (/^[a-f0-9]{64}$/i.test(value)) {
1354
- return value.toLowerCase();
1355
- }
1356
- if (typeof globalThis !== "undefined" && globalThis.crypto?.subtle) {
1357
- const encoder = new TextEncoder();
1358
- const data = encoder.encode(value);
1359
- const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", data);
1360
- const hashArray = Array.from(new Uint8Array(hashBuffer));
1361
- return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
1362
- }
1363
- const nodeCrypto = await import("crypto");
1364
- return nodeCrypto.createHash("sha256").update(value).digest("hex");
1387
+ return value;
1365
1388
  }
1366
1389
  };
1367
1390
 
@@ -1,5 +1,5 @@
1
- import { S as SSOConfig, U as UseSSOReturn, a as SupabaseSession, b as SSOSyncConfig, c as UseSSOSyncReturn, d as SSOUser } from '../constants-BOZ6jJU4.cjs';
2
- export { C as COOKIE_NAMES, D as DEFAULT_AUTH_GATEWAY, f as DEFAULT_COOKIE_DOMAIN, g as DEFAULT_POLL_INTERVAL, h as DEFAULT_PROJECT_SCOPE, e as SSOState } from '../constants-BOZ6jJU4.cjs';
1
+ import { S as SSOConfig, U as UseSSOReturn, a as SupabaseSession, b as SSOSyncConfig, c as UseSSOSyncReturn, d as SSOUser } from '../constants-BZPTHasL.cjs';
2
+ export { C as COOKIE_NAMES, D as DEFAULT_AUTH_GATEWAY, e as DEFAULT_COOKIE_DOMAIN, f as DEFAULT_POLL_INTERVAL, g as DEFAULT_PROJECT_SCOPE, h as SSOState } from '../constants-BZPTHasL.cjs';
3
3
 
4
4
  /**
5
5
  * React hook for SSO authentication state
@@ -1,5 +1,5 @@
1
- import { S as SSOConfig, U as UseSSOReturn, a as SupabaseSession, b as SSOSyncConfig, c as UseSSOSyncReturn, d as SSOUser } from '../constants-BOZ6jJU4.js';
2
- export { C as COOKIE_NAMES, D as DEFAULT_AUTH_GATEWAY, f as DEFAULT_COOKIE_DOMAIN, g as DEFAULT_POLL_INTERVAL, h as DEFAULT_PROJECT_SCOPE, e as SSOState } from '../constants-BOZ6jJU4.js';
1
+ import { S as SSOConfig, U as UseSSOReturn, a as SupabaseSession, b as SSOSyncConfig, c as UseSSOSyncReturn, d as SSOUser } from '../constants-BZPTHasL.js';
2
+ export { C as COOKIE_NAMES, D as DEFAULT_AUTH_GATEWAY, e as DEFAULT_COOKIE_DOMAIN, f as DEFAULT_POLL_INTERVAL, g as DEFAULT_PROJECT_SCOPE, h as SSOState } from '../constants-BZPTHasL.js';
3
3
 
4
4
  /**
5
5
  * React hook for SSO authentication state
@@ -1,5 +1,5 @@
1
- import { d as SSOUser } from '../constants-BOZ6jJU4.cjs';
2
- export { C as COOKIE_NAMES, D as DEFAULT_AUTH_GATEWAY, f as DEFAULT_COOKIE_DOMAIN, h as DEFAULT_PROJECT_SCOPE } from '../constants-BOZ6jJU4.cjs';
1
+ import { d as SSOUser } from '../constants-BZPTHasL.cjs';
2
+ export { C as COOKIE_NAMES, D as DEFAULT_AUTH_GATEWAY, e as DEFAULT_COOKIE_DOMAIN, g as DEFAULT_PROJECT_SCOPE } from '../constants-BZPTHasL.cjs';
3
3
 
4
4
  /**
5
5
  * Server-side types for SSO authentication
@@ -1,5 +1,5 @@
1
- import { d as SSOUser } from '../constants-BOZ6jJU4.js';
2
- export { C as COOKIE_NAMES, D as DEFAULT_AUTH_GATEWAY, f as DEFAULT_COOKIE_DOMAIN, h as DEFAULT_PROJECT_SCOPE } from '../constants-BOZ6jJU4.js';
1
+ import { d as SSOUser } from '../constants-BZPTHasL.js';
2
+ export { C as COOKIE_NAMES, D as DEFAULT_AUTH_GATEWAY, e as DEFAULT_COOKIE_DOMAIN, g as DEFAULT_PROJECT_SCOPE } from '../constants-BZPTHasL.js';
3
3
 
4
4
  /**
5
5
  * Server-side types for SSO authentication
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/oauth-client",
3
- "version": "2.0.0",
3
+ "version": "2.0.4",
4
4
  "type": "module",
5
5
  "description": "OAuth and API Key authentication client for Lan Onasis MCP integration",
6
6
  "license": "MIT",
@@ -43,6 +43,27 @@
43
43
  "types": "./dist/server/index.d.ts",
44
44
  "import": "./dist/server/index.mjs",
45
45
  "require": "./dist/server/index.cjs"
46
+ },
47
+ "./cookies": {
48
+ "types": "./dist/cookies/constants.d.ts",
49
+ "import": "./dist/cookies/constants.mjs",
50
+ "require": "./dist/cookies/constants.cjs"
51
+ }
52
+ },
53
+ "typesVersions": {
54
+ "*": {
55
+ "browser": [
56
+ "./dist/browser.d.ts"
57
+ ],
58
+ "react": [
59
+ "./dist/react/index.d.ts"
60
+ ],
61
+ "server": [
62
+ "./dist/server/index.d.ts"
63
+ ],
64
+ "cookies": [
65
+ "./dist/cookies/constants.d.ts"
66
+ ]
46
67
  }
47
68
  },
48
69
  "scripts": {
@@ -63,25 +84,30 @@
63
84
  "desktop"
64
85
  ],
65
86
  "dependencies": {
66
- "cross-fetch": "^4.0.0",
67
- "eventsource": "^2.0.2",
87
+ "cross-fetch": "^4.1.0",
88
+ "eventsource": "^4.1.0",
68
89
  "keytar": "^7.9.0",
69
- "open": "^9.1.0"
90
+ "open": "^11.0.0"
70
91
  },
71
92
  "devDependencies": {
72
- "@eslint/js": "^9.17.0",
73
- "@types/node": "^20.0.0",
74
- "@types/react": "^19.0.0",
75
- "@vitest/coverage-v8": "^2.0.0",
76
- "eslint": "^9.17.0",
77
- "globals": "^16.4.0",
78
- "tsup": "^8.5.0",
79
- "typescript": "^5.3.3",
80
- "typescript-eslint": "^8.18.1",
81
- "vitest": "^2.0.0"
93
+ "@eslint/js": "^10.0.1",
94
+ "@types/node": "^25.5.0",
95
+ "@types/react": "^19.2.14",
96
+ "@vitest/coverage-v8": "^4.1.2",
97
+ "eslint": "^10.1.0",
98
+ "globals": "^17.4.0",
99
+ "tsup": "^8.5.1",
100
+ "typescript": "^5.9.3",
101
+ "typescript-eslint": "^8.57.2",
102
+ "vitest": "^4.1.2"
103
+ },
104
+ "overrides": {
105
+ "flatted": "^3.4.0",
106
+ "picomatch": "^4.0.4",
107
+ "rollup": "^4.59.0"
82
108
  },
83
109
  "peerDependencies": {
84
- "@supabase/supabase-js": "^2.0.0",
110
+ "@supabase/supabase-js": "^2.100.1",
85
111
  "react": "^18.0.0 || ^19.0.0"
86
112
  },
87
113
  "peerDependenciesMeta": {