@lanonasis/oauth-client 1.2.7 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.cjs CHANGED
@@ -433,7 +433,7 @@ var APIKeyFlow = class extends BaseOAuthFlow {
433
433
  */
434
434
  async validateAPIKey() {
435
435
  try {
436
- const response = await (0, import_cross_fetch2.default)(`${this.config.authBaseUrl}/api/v1/health`, {
436
+ const response = await (0, import_cross_fetch2.default)(`${this.authBaseUrl}/api/v1/health`, {
437
437
  headers: {
438
438
  "x-api-key": this.apiKey
439
439
  }
package/dist/browser.mjs CHANGED
@@ -399,7 +399,7 @@ var APIKeyFlow = class extends BaseOAuthFlow {
399
399
  */
400
400
  async validateAPIKey() {
401
401
  try {
402
- const response = await fetch2(`${this.config.authBaseUrl}/api/v1/health`, {
402
+ const response = await fetch2(`${this.authBaseUrl}/api/v1/health`, {
403
403
  headers: {
404
404
  "x-api-key": this.apiKey
405
405
  }
package/dist/index.cjs CHANGED
@@ -30,12 +30,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ APIKeyFlow: () => APIKeyFlow,
33
34
  ApiKeyStorage: () => ApiKeyStorage,
34
35
  ApiKeyStorageWeb: () => ApiKeyStorageWeb,
35
36
  AuthGatewayClient: () => AuthGatewayClient,
36
37
  BaseOAuthFlow: () => BaseOAuthFlow,
37
38
  DesktopOAuthFlow: () => DesktopOAuthFlow,
38
39
  MCPClient: () => MCPClient,
40
+ MagicLinkFlow: () => MagicLinkFlow,
39
41
  TerminalOAuthFlow: () => TerminalOAuthFlow,
40
42
  TokenStorage: () => TokenStorage,
41
43
  TokenStorageWeb: () => TokenStorageWeb
@@ -368,6 +370,256 @@ var DesktopOAuthFlow = class extends BaseOAuthFlow {
368
370
  }
369
371
  };
370
372
 
373
+ // src/flows/magic-link-flow.ts
374
+ var import_cross_fetch3 = __toESM(require("cross-fetch"), 1);
375
+ var MagicLinkFlow = class extends BaseOAuthFlow {
376
+ constructor(config) {
377
+ super(config);
378
+ this.projectScope = config.projectScope || "lanonasis-maas";
379
+ this.platform = config.platform || "cli";
380
+ }
381
+ /**
382
+ * Main authenticate method - uses OTP code flow by default
383
+ * For interactive CLI usage, prefer using requestOTP() and verifyOTP() separately
384
+ */
385
+ async authenticate() {
386
+ throw new Error(
387
+ "MagicLinkFlow requires two-step authentication. Use requestOTP() + verifyOTP() for CLI, or requestMagicLink() for web."
388
+ );
389
+ }
390
+ // ============================================================================
391
+ // OTP Code Flow (for CLI, mobile - user enters code manually)
392
+ // ============================================================================
393
+ /**
394
+ * Request a 6-digit OTP code to be sent via email
395
+ * User will enter this code manually in the CLI
396
+ *
397
+ * @param email - User's email address
398
+ * @returns Response with success status and expiration time
399
+ */
400
+ async requestOTP(email) {
401
+ const response = await (0, import_cross_fetch3.default)(`${this.authBaseUrl}/v1/auth/otp/send`, {
402
+ method: "POST",
403
+ headers: { "Content-Type": "application/json" },
404
+ body: JSON.stringify({
405
+ email: email.trim().toLowerCase(),
406
+ type: "email",
407
+ // Explicitly request 6-digit code, not magic link
408
+ platform: this.platform,
409
+ project_scope: this.projectScope
410
+ })
411
+ });
412
+ const data = await response.json();
413
+ if (!response.ok) {
414
+ throw new Error(data.message || data.error || "Failed to send OTP");
415
+ }
416
+ return data;
417
+ }
418
+ /**
419
+ * Verify the OTP code entered by the user and get tokens
420
+ *
421
+ * @param email - User's email address (must match the one used in requestOTP)
422
+ * @param code - 6-digit OTP code from email
423
+ * @returns Token response with access_token, refresh_token, etc.
424
+ */
425
+ async verifyOTP(email, code) {
426
+ const response = await (0, import_cross_fetch3.default)(`${this.authBaseUrl}/v1/auth/otp/verify`, {
427
+ method: "POST",
428
+ headers: { "Content-Type": "application/json" },
429
+ body: JSON.stringify({
430
+ email: email.trim().toLowerCase(),
431
+ token: code.trim(),
432
+ type: "email",
433
+ platform: this.platform,
434
+ project_scope: this.projectScope
435
+ })
436
+ });
437
+ const data = await response.json();
438
+ if (!response.ok) {
439
+ throw new Error(data.message || data.error || "Invalid or expired OTP code");
440
+ }
441
+ return data;
442
+ }
443
+ /**
444
+ * Resend OTP code (rate limited)
445
+ *
446
+ * @param email - User's email address
447
+ * @returns Response with success status
448
+ */
449
+ async resendOTP(email) {
450
+ const response = await (0, import_cross_fetch3.default)(`${this.authBaseUrl}/v1/auth/otp/resend`, {
451
+ method: "POST",
452
+ headers: { "Content-Type": "application/json" },
453
+ body: JSON.stringify({
454
+ email: email.trim().toLowerCase(),
455
+ type: "email",
456
+ platform: this.platform
457
+ })
458
+ });
459
+ const data = await response.json();
460
+ if (!response.ok) {
461
+ if (response.status === 429) {
462
+ throw new Error("Rate limited. Please wait before requesting another code.");
463
+ }
464
+ throw new Error(data.message || data.error || "Failed to resend OTP");
465
+ }
466
+ return data;
467
+ }
468
+ // ============================================================================
469
+ // Magic Link Flow (for web, desktop - user clicks link in email)
470
+ // ============================================================================
471
+ /**
472
+ * Request a magic link to be sent via email
473
+ * User will click the link which redirects to your callback URL
474
+ *
475
+ * @param email - User's email address
476
+ * @param redirectUri - URL to redirect to after clicking the magic link
477
+ * @returns Response with success status
478
+ */
479
+ async requestMagicLink(email, redirectUri) {
480
+ const response = await (0, import_cross_fetch3.default)(`${this.authBaseUrl}/v1/auth/otp/send`, {
481
+ method: "POST",
482
+ headers: { "Content-Type": "application/json" },
483
+ body: JSON.stringify({
484
+ email: email.trim().toLowerCase(),
485
+ type: "magiclink",
486
+ redirect_uri: redirectUri,
487
+ platform: this.platform,
488
+ project_scope: this.projectScope
489
+ })
490
+ });
491
+ const data = await response.json();
492
+ if (!response.ok) {
493
+ throw new Error(data.message || data.error || "Failed to send magic link");
494
+ }
495
+ return data;
496
+ }
497
+ /**
498
+ * Alternative: Use the /v1/auth/magic-link endpoint (web-optimized)
499
+ * This endpoint provides better redirect handling for web apps
500
+ *
501
+ * @param email - User's email address
502
+ * @param redirectUri - URL to redirect to after authentication
503
+ * @param createUser - Whether to create a new user if email doesn't exist
504
+ */
505
+ async requestMagicLinkWeb(email, redirectUri, createUser = true) {
506
+ const response = await (0, import_cross_fetch3.default)(`${this.authBaseUrl}/v1/auth/magic-link`, {
507
+ method: "POST",
508
+ headers: { "Content-Type": "application/json" },
509
+ body: JSON.stringify({
510
+ email: email.trim().toLowerCase(),
511
+ redirect_uri: redirectUri,
512
+ return_to: redirectUri,
513
+ // Alias for compatibility
514
+ project_scope: this.projectScope,
515
+ platform: "web",
516
+ create_user: createUser
517
+ })
518
+ });
519
+ const data = await response.json();
520
+ if (!response.ok) {
521
+ throw new Error(data.message || data.error || "Failed to send magic link");
522
+ }
523
+ return data;
524
+ }
525
+ /**
526
+ * Exchange magic link token for auth-gateway tokens
527
+ * Called after user clicks the magic link and is redirected to your callback
528
+ *
529
+ * @param supabaseAccessToken - Access token from Supabase (from URL hash/query)
530
+ * @param state - State parameter from the callback URL
531
+ * @returns Token response with redirect URL
532
+ */
533
+ async exchangeMagicLinkToken(supabaseAccessToken, state) {
534
+ const response = await (0, import_cross_fetch3.default)(`${this.authBaseUrl}/v1/auth/magic-link/exchange`, {
535
+ method: "POST",
536
+ headers: {
537
+ "Content-Type": "application/json",
538
+ "Authorization": `Bearer ${supabaseAccessToken}`
539
+ },
540
+ body: JSON.stringify({ state })
541
+ });
542
+ const data = await response.json();
543
+ if (!response.ok) {
544
+ throw new Error(data.message || data.error || "Magic link exchange failed");
545
+ }
546
+ return data;
547
+ }
548
+ // ============================================================================
549
+ // Utility Methods
550
+ // ============================================================================
551
+ /**
552
+ * Check if an email is valid format
553
+ */
554
+ static isValidEmail(email) {
555
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
556
+ return emailRegex.test(email.trim());
557
+ }
558
+ /**
559
+ * Check if an OTP code is valid format (6 digits)
560
+ */
561
+ static isValidOTPCode(code) {
562
+ return /^\d{6}$/.test(code.trim());
563
+ }
564
+ };
565
+
566
+ // src/flows/apikey-flow.ts
567
+ var import_cross_fetch4 = __toESM(require("cross-fetch"), 1);
568
+ var APIKeyFlow = class extends BaseOAuthFlow {
569
+ constructor(apiKey, authBaseUrl = "https://mcp.lanonasis.com") {
570
+ super({
571
+ clientId: "api-key-client",
572
+ authBaseUrl
573
+ });
574
+ this.apiKey = apiKey;
575
+ }
576
+ /**
577
+ * "Authenticate" by returning the API key as a virtual token
578
+ * The API key will be used directly in request headers
579
+ */
580
+ async authenticate() {
581
+ if (!this.apiKey || !this.apiKey.startsWith("lano_") && !this.apiKey.startsWith("vx_")) {
582
+ throw new Error(
583
+ 'Invalid API key format. Must start with "lano_" or "vx_". Please regenerate your API key from the dashboard.'
584
+ );
585
+ }
586
+ if (this.apiKey.startsWith("vx_")) {
587
+ console.warn(
588
+ '\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.'
589
+ );
590
+ }
591
+ return {
592
+ access_token: this.apiKey,
593
+ token_type: "api-key",
594
+ expires_in: 0,
595
+ // API keys don't expire
596
+ issued_at: Date.now()
597
+ };
598
+ }
599
+ /**
600
+ * API keys don't need refresh
601
+ */
602
+ async refreshToken(refreshToken) {
603
+ throw new Error("API keys do not support token refresh");
604
+ }
605
+ /**
606
+ * Optional: Validate API key by making a test request
607
+ */
608
+ async validateAPIKey() {
609
+ try {
610
+ const response = await (0, import_cross_fetch4.default)(`${this.authBaseUrl}/api/v1/health`, {
611
+ headers: {
612
+ "x-api-key": this.apiKey
613
+ }
614
+ });
615
+ return response.ok;
616
+ } catch (error) {
617
+ console.error("API key validation failed:", error);
618
+ return false;
619
+ }
620
+ }
621
+ };
622
+
371
623
  // src/storage/token-storage.ts
372
624
  var _fs = null;
373
625
  var _path = null;
@@ -1405,66 +1657,7 @@ var ApiKeyStorageWeb = class {
1405
1657
  };
1406
1658
 
1407
1659
  // src/client/mcp-client.ts
1408
- var import_cross_fetch4 = __toESM(require("cross-fetch"), 1);
1409
-
1410
- // src/flows/apikey-flow.ts
1411
- var import_cross_fetch3 = __toESM(require("cross-fetch"), 1);
1412
- var APIKeyFlow = class extends BaseOAuthFlow {
1413
- constructor(apiKey, authBaseUrl = "https://mcp.lanonasis.com") {
1414
- super({
1415
- clientId: "api-key-client",
1416
- authBaseUrl
1417
- });
1418
- this.apiKey = apiKey;
1419
- }
1420
- /**
1421
- * "Authenticate" by returning the API key as a virtual token
1422
- * The API key will be used directly in request headers
1423
- */
1424
- async authenticate() {
1425
- if (!this.apiKey || !this.apiKey.startsWith("lano_") && !this.apiKey.startsWith("vx_")) {
1426
- throw new Error(
1427
- 'Invalid API key format. Must start with "lano_" or "vx_". Please regenerate your API key from the dashboard.'
1428
- );
1429
- }
1430
- if (this.apiKey.startsWith("vx_")) {
1431
- console.warn(
1432
- '\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.'
1433
- );
1434
- }
1435
- return {
1436
- access_token: this.apiKey,
1437
- token_type: "api-key",
1438
- expires_in: 0,
1439
- // API keys don't expire
1440
- issued_at: Date.now()
1441
- };
1442
- }
1443
- /**
1444
- * API keys don't need refresh
1445
- */
1446
- async refreshToken(refreshToken) {
1447
- throw new Error("API keys do not support token refresh");
1448
- }
1449
- /**
1450
- * Optional: Validate API key by making a test request
1451
- */
1452
- async validateAPIKey() {
1453
- try {
1454
- const response = await (0, import_cross_fetch3.default)(`${this.config.authBaseUrl}/api/v1/health`, {
1455
- headers: {
1456
- "x-api-key": this.apiKey
1457
- }
1458
- });
1459
- return response.ok;
1460
- } catch (error) {
1461
- console.error("API key validation failed:", error);
1462
- return false;
1463
- }
1464
- }
1465
- };
1466
-
1467
- // src/client/mcp-client.ts
1660
+ var import_cross_fetch5 = __toESM(require("cross-fetch"), 1);
1468
1661
  var MCPClient = class {
1469
1662
  constructor(config = {}) {
1470
1663
  // ← NEW: Track auth mode
@@ -1693,7 +1886,7 @@ var MCPClient = class {
1693
1886
  } else {
1694
1887
  headers["Authorization"] = `Bearer ${this.accessToken}`;
1695
1888
  }
1696
- const response = await (0, import_cross_fetch4.default)(`${this.config.mcpEndpoint}/api`, {
1889
+ const response = await (0, import_cross_fetch5.default)(`${this.config.mcpEndpoint}/api`, {
1697
1890
  method: "POST",
1698
1891
  headers,
1699
1892
  body: JSON.stringify({
@@ -1794,7 +1987,7 @@ var MCPClient = class {
1794
1987
  };
1795
1988
 
1796
1989
  // src/client/auth-gateway-client.ts
1797
- var import_cross_fetch5 = __toESM(require("cross-fetch"), 1);
1990
+ var import_cross_fetch6 = __toESM(require("cross-fetch"), 1);
1798
1991
  var GatewayOAuthFlow = class extends BaseOAuthFlow {
1799
1992
  async authenticate() {
1800
1993
  throw new Error("Interactive authentication is not supported in AuthGatewayClient.");
@@ -1958,7 +2151,7 @@ var AuthGatewayClient = class {
1958
2151
  return "jwt";
1959
2152
  }
1960
2153
  async requestJson(path, options) {
1961
- const response = await (0, import_cross_fetch5.default)(`${this.authBaseUrl}${path.startsWith("/") ? path : `/${path}`}`, options);
2154
+ const response = await (0, import_cross_fetch6.default)(`${this.authBaseUrl}${path.startsWith("/") ? path : `/${path}`}`, options);
1962
2155
  const text = await response.text();
1963
2156
  let data = null;
1964
2157
  if (text) {
package/dist/index.d.cts CHANGED
@@ -13,6 +13,153 @@ declare class TerminalOAuthFlow extends BaseOAuthFlow {
13
13
  private checkDeviceCode;
14
14
  }
15
15
 
16
+ /**
17
+ * Magic Link / OTP Flow
18
+ *
19
+ * Passwordless authentication supporting two modes:
20
+ * 1. OTP Code (type: 'email') - User receives 6-digit code to enter manually (CLI, mobile)
21
+ * 2. Magic Link (type: 'magiclink') - User clicks link in email (web, desktop)
22
+ *
23
+ * Usage:
24
+ * ```typescript
25
+ * // OTP Code Flow (CLI)
26
+ * const flow = new MagicLinkFlow({ clientId: 'my-app' });
27
+ * await flow.requestOTP('user@example.com');
28
+ * const tokens = await flow.verifyOTP('user@example.com', '123456');
29
+ *
30
+ * // Magic Link Flow (Web)
31
+ * const flow = new MagicLinkFlow({ clientId: 'my-app' });
32
+ * await flow.requestMagicLink('user@example.com', 'https://myapp.com/auth/callback');
33
+ * // User clicks link in email, then:
34
+ * const tokens = await flow.exchangeMagicLinkToken(supabaseAccessToken, state);
35
+ * ```
36
+ */
37
+
38
+ type OTPType = 'email' | 'magiclink';
39
+ type Platform = 'cli' | 'web' | 'mcp' | 'api';
40
+ interface MagicLinkConfig extends OAuthConfig {
41
+ projectScope?: string;
42
+ platform?: Platform;
43
+ }
44
+ interface OTPSendResponse {
45
+ success: boolean;
46
+ message: string;
47
+ type: OTPType;
48
+ expires_in: number;
49
+ }
50
+ interface OTPVerifyResponse extends TokenResponse {
51
+ auth_method: 'otp' | 'magic_link';
52
+ user?: {
53
+ id: string;
54
+ email: string;
55
+ role: string;
56
+ };
57
+ }
58
+ interface MagicLinkExchangeResponse extends TokenResponse {
59
+ redirect_to?: string;
60
+ user?: {
61
+ id: string;
62
+ email?: string;
63
+ role?: string;
64
+ project_scope?: string;
65
+ };
66
+ }
67
+ declare class MagicLinkFlow extends BaseOAuthFlow {
68
+ private readonly projectScope;
69
+ private readonly platform;
70
+ constructor(config: MagicLinkConfig);
71
+ /**
72
+ * Main authenticate method - uses OTP code flow by default
73
+ * For interactive CLI usage, prefer using requestOTP() and verifyOTP() separately
74
+ */
75
+ authenticate(): Promise<TokenResponse>;
76
+ /**
77
+ * Request a 6-digit OTP code to be sent via email
78
+ * User will enter this code manually in the CLI
79
+ *
80
+ * @param email - User's email address
81
+ * @returns Response with success status and expiration time
82
+ */
83
+ requestOTP(email: string): Promise<OTPSendResponse>;
84
+ /**
85
+ * Verify the OTP code entered by the user and get tokens
86
+ *
87
+ * @param email - User's email address (must match the one used in requestOTP)
88
+ * @param code - 6-digit OTP code from email
89
+ * @returns Token response with access_token, refresh_token, etc.
90
+ */
91
+ verifyOTP(email: string, code: string): Promise<OTPVerifyResponse>;
92
+ /**
93
+ * Resend OTP code (rate limited)
94
+ *
95
+ * @param email - User's email address
96
+ * @returns Response with success status
97
+ */
98
+ resendOTP(email: string): Promise<OTPSendResponse>;
99
+ /**
100
+ * Request a magic link to be sent via email
101
+ * User will click the link which redirects to your callback URL
102
+ *
103
+ * @param email - User's email address
104
+ * @param redirectUri - URL to redirect to after clicking the magic link
105
+ * @returns Response with success status
106
+ */
107
+ requestMagicLink(email: string, redirectUri: string): Promise<OTPSendResponse>;
108
+ /**
109
+ * Alternative: Use the /v1/auth/magic-link endpoint (web-optimized)
110
+ * This endpoint provides better redirect handling for web apps
111
+ *
112
+ * @param email - User's email address
113
+ * @param redirectUri - URL to redirect to after authentication
114
+ * @param createUser - Whether to create a new user if email doesn't exist
115
+ */
116
+ requestMagicLinkWeb(email: string, redirectUri: string, createUser?: boolean): Promise<{
117
+ success: boolean;
118
+ message: string;
119
+ }>;
120
+ /**
121
+ * Exchange magic link token for auth-gateway tokens
122
+ * Called after user clicks the magic link and is redirected to your callback
123
+ *
124
+ * @param supabaseAccessToken - Access token from Supabase (from URL hash/query)
125
+ * @param state - State parameter from the callback URL
126
+ * @returns Token response with redirect URL
127
+ */
128
+ exchangeMagicLinkToken(supabaseAccessToken: string, state: string): Promise<MagicLinkExchangeResponse>;
129
+ /**
130
+ * Check if an email is valid format
131
+ */
132
+ static isValidEmail(email: string): boolean;
133
+ /**
134
+ * Check if an OTP code is valid format (6 digits)
135
+ */
136
+ static isValidOTPCode(code: string): boolean;
137
+ }
138
+
139
+ /**
140
+ * API Key Authentication Flow
141
+ *
142
+ * This flow uses a direct API key for authentication instead of OAuth.
143
+ * The API key is sent via x-api-key header to the backend.
144
+ */
145
+ declare class APIKeyFlow extends BaseOAuthFlow {
146
+ private apiKey;
147
+ constructor(apiKey: string, authBaseUrl?: string);
148
+ /**
149
+ * "Authenticate" by returning the API key as a virtual token
150
+ * The API key will be used directly in request headers
151
+ */
152
+ authenticate(): Promise<TokenResponse>;
153
+ /**
154
+ * API keys don't need refresh
155
+ */
156
+ refreshToken(refreshToken: string): Promise<TokenResponse>;
157
+ /**
158
+ * Optional: Validate API key by making a test request
159
+ */
160
+ validateAPIKey(): Promise<boolean>;
161
+ }
162
+
16
163
  interface MCPClientConfig extends Partial<OAuthConfig> {
17
164
  mcpEndpoint?: string;
18
165
  autoRefresh?: boolean;
@@ -51,4 +198,4 @@ declare class MCPClient {
51
198
  deleteMemory(id: string): Promise<void>;
52
199
  }
53
200
 
54
- export { BaseOAuthFlow, MCPClient, type MCPClientConfig, OAuthConfig, TerminalOAuthFlow, TokenResponse, TokenStorageAdapter };
201
+ export { APIKeyFlow, BaseOAuthFlow, MCPClient, type MCPClientConfig, type MagicLinkConfig, type MagicLinkExchangeResponse, MagicLinkFlow, OAuthConfig, type OTPSendResponse, type OTPType, type OTPVerifyResponse, type Platform, TerminalOAuthFlow, TokenResponse, TokenStorageAdapter };
package/dist/index.d.ts CHANGED
@@ -13,6 +13,153 @@ declare class TerminalOAuthFlow extends BaseOAuthFlow {
13
13
  private checkDeviceCode;
14
14
  }
15
15
 
16
+ /**
17
+ * Magic Link / OTP Flow
18
+ *
19
+ * Passwordless authentication supporting two modes:
20
+ * 1. OTP Code (type: 'email') - User receives 6-digit code to enter manually (CLI, mobile)
21
+ * 2. Magic Link (type: 'magiclink') - User clicks link in email (web, desktop)
22
+ *
23
+ * Usage:
24
+ * ```typescript
25
+ * // OTP Code Flow (CLI)
26
+ * const flow = new MagicLinkFlow({ clientId: 'my-app' });
27
+ * await flow.requestOTP('user@example.com');
28
+ * const tokens = await flow.verifyOTP('user@example.com', '123456');
29
+ *
30
+ * // Magic Link Flow (Web)
31
+ * const flow = new MagicLinkFlow({ clientId: 'my-app' });
32
+ * await flow.requestMagicLink('user@example.com', 'https://myapp.com/auth/callback');
33
+ * // User clicks link in email, then:
34
+ * const tokens = await flow.exchangeMagicLinkToken(supabaseAccessToken, state);
35
+ * ```
36
+ */
37
+
38
+ type OTPType = 'email' | 'magiclink';
39
+ type Platform = 'cli' | 'web' | 'mcp' | 'api';
40
+ interface MagicLinkConfig extends OAuthConfig {
41
+ projectScope?: string;
42
+ platform?: Platform;
43
+ }
44
+ interface OTPSendResponse {
45
+ success: boolean;
46
+ message: string;
47
+ type: OTPType;
48
+ expires_in: number;
49
+ }
50
+ interface OTPVerifyResponse extends TokenResponse {
51
+ auth_method: 'otp' | 'magic_link';
52
+ user?: {
53
+ id: string;
54
+ email: string;
55
+ role: string;
56
+ };
57
+ }
58
+ interface MagicLinkExchangeResponse extends TokenResponse {
59
+ redirect_to?: string;
60
+ user?: {
61
+ id: string;
62
+ email?: string;
63
+ role?: string;
64
+ project_scope?: string;
65
+ };
66
+ }
67
+ declare class MagicLinkFlow extends BaseOAuthFlow {
68
+ private readonly projectScope;
69
+ private readonly platform;
70
+ constructor(config: MagicLinkConfig);
71
+ /**
72
+ * Main authenticate method - uses OTP code flow by default
73
+ * For interactive CLI usage, prefer using requestOTP() and verifyOTP() separately
74
+ */
75
+ authenticate(): Promise<TokenResponse>;
76
+ /**
77
+ * Request a 6-digit OTP code to be sent via email
78
+ * User will enter this code manually in the CLI
79
+ *
80
+ * @param email - User's email address
81
+ * @returns Response with success status and expiration time
82
+ */
83
+ requestOTP(email: string): Promise<OTPSendResponse>;
84
+ /**
85
+ * Verify the OTP code entered by the user and get tokens
86
+ *
87
+ * @param email - User's email address (must match the one used in requestOTP)
88
+ * @param code - 6-digit OTP code from email
89
+ * @returns Token response with access_token, refresh_token, etc.
90
+ */
91
+ verifyOTP(email: string, code: string): Promise<OTPVerifyResponse>;
92
+ /**
93
+ * Resend OTP code (rate limited)
94
+ *
95
+ * @param email - User's email address
96
+ * @returns Response with success status
97
+ */
98
+ resendOTP(email: string): Promise<OTPSendResponse>;
99
+ /**
100
+ * Request a magic link to be sent via email
101
+ * User will click the link which redirects to your callback URL
102
+ *
103
+ * @param email - User's email address
104
+ * @param redirectUri - URL to redirect to after clicking the magic link
105
+ * @returns Response with success status
106
+ */
107
+ requestMagicLink(email: string, redirectUri: string): Promise<OTPSendResponse>;
108
+ /**
109
+ * Alternative: Use the /v1/auth/magic-link endpoint (web-optimized)
110
+ * This endpoint provides better redirect handling for web apps
111
+ *
112
+ * @param email - User's email address
113
+ * @param redirectUri - URL to redirect to after authentication
114
+ * @param createUser - Whether to create a new user if email doesn't exist
115
+ */
116
+ requestMagicLinkWeb(email: string, redirectUri: string, createUser?: boolean): Promise<{
117
+ success: boolean;
118
+ message: string;
119
+ }>;
120
+ /**
121
+ * Exchange magic link token for auth-gateway tokens
122
+ * Called after user clicks the magic link and is redirected to your callback
123
+ *
124
+ * @param supabaseAccessToken - Access token from Supabase (from URL hash/query)
125
+ * @param state - State parameter from the callback URL
126
+ * @returns Token response with redirect URL
127
+ */
128
+ exchangeMagicLinkToken(supabaseAccessToken: string, state: string): Promise<MagicLinkExchangeResponse>;
129
+ /**
130
+ * Check if an email is valid format
131
+ */
132
+ static isValidEmail(email: string): boolean;
133
+ /**
134
+ * Check if an OTP code is valid format (6 digits)
135
+ */
136
+ static isValidOTPCode(code: string): boolean;
137
+ }
138
+
139
+ /**
140
+ * API Key Authentication Flow
141
+ *
142
+ * This flow uses a direct API key for authentication instead of OAuth.
143
+ * The API key is sent via x-api-key header to the backend.
144
+ */
145
+ declare class APIKeyFlow extends BaseOAuthFlow {
146
+ private apiKey;
147
+ constructor(apiKey: string, authBaseUrl?: string);
148
+ /**
149
+ * "Authenticate" by returning the API key as a virtual token
150
+ * The API key will be used directly in request headers
151
+ */
152
+ authenticate(): Promise<TokenResponse>;
153
+ /**
154
+ * API keys don't need refresh
155
+ */
156
+ refreshToken(refreshToken: string): Promise<TokenResponse>;
157
+ /**
158
+ * Optional: Validate API key by making a test request
159
+ */
160
+ validateAPIKey(): Promise<boolean>;
161
+ }
162
+
16
163
  interface MCPClientConfig extends Partial<OAuthConfig> {
17
164
  mcpEndpoint?: string;
18
165
  autoRefresh?: boolean;
@@ -51,4 +198,4 @@ declare class MCPClient {
51
198
  deleteMemory(id: string): Promise<void>;
52
199
  }
53
200
 
54
- export { BaseOAuthFlow, MCPClient, type MCPClientConfig, OAuthConfig, TerminalOAuthFlow, TokenResponse, TokenStorageAdapter };
201
+ export { APIKeyFlow, BaseOAuthFlow, MCPClient, type MCPClientConfig, type MagicLinkConfig, type MagicLinkExchangeResponse, MagicLinkFlow, OAuthConfig, type OTPSendResponse, type OTPType, type OTPVerifyResponse, type Platform, TerminalOAuthFlow, TokenResponse, TokenStorageAdapter };
package/dist/index.mjs CHANGED
@@ -331,6 +331,256 @@ var DesktopOAuthFlow = class extends BaseOAuthFlow {
331
331
  }
332
332
  };
333
333
 
334
+ // src/flows/magic-link-flow.ts
335
+ import fetch3 from "cross-fetch";
336
+ var MagicLinkFlow = class extends BaseOAuthFlow {
337
+ constructor(config) {
338
+ super(config);
339
+ this.projectScope = config.projectScope || "lanonasis-maas";
340
+ this.platform = config.platform || "cli";
341
+ }
342
+ /**
343
+ * Main authenticate method - uses OTP code flow by default
344
+ * For interactive CLI usage, prefer using requestOTP() and verifyOTP() separately
345
+ */
346
+ async authenticate() {
347
+ throw new Error(
348
+ "MagicLinkFlow requires two-step authentication. Use requestOTP() + verifyOTP() for CLI, or requestMagicLink() for web."
349
+ );
350
+ }
351
+ // ============================================================================
352
+ // OTP Code Flow (for CLI, mobile - user enters code manually)
353
+ // ============================================================================
354
+ /**
355
+ * Request a 6-digit OTP code to be sent via email
356
+ * User will enter this code manually in the CLI
357
+ *
358
+ * @param email - User's email address
359
+ * @returns Response with success status and expiration time
360
+ */
361
+ async requestOTP(email) {
362
+ const response = await fetch3(`${this.authBaseUrl}/v1/auth/otp/send`, {
363
+ method: "POST",
364
+ headers: { "Content-Type": "application/json" },
365
+ body: JSON.stringify({
366
+ email: email.trim().toLowerCase(),
367
+ type: "email",
368
+ // Explicitly request 6-digit code, not magic link
369
+ platform: this.platform,
370
+ project_scope: this.projectScope
371
+ })
372
+ });
373
+ const data = await response.json();
374
+ if (!response.ok) {
375
+ throw new Error(data.message || data.error || "Failed to send OTP");
376
+ }
377
+ return data;
378
+ }
379
+ /**
380
+ * Verify the OTP code entered by the user and get tokens
381
+ *
382
+ * @param email - User's email address (must match the one used in requestOTP)
383
+ * @param code - 6-digit OTP code from email
384
+ * @returns Token response with access_token, refresh_token, etc.
385
+ */
386
+ async verifyOTP(email, code) {
387
+ const response = await fetch3(`${this.authBaseUrl}/v1/auth/otp/verify`, {
388
+ method: "POST",
389
+ headers: { "Content-Type": "application/json" },
390
+ body: JSON.stringify({
391
+ email: email.trim().toLowerCase(),
392
+ token: code.trim(),
393
+ type: "email",
394
+ platform: this.platform,
395
+ project_scope: this.projectScope
396
+ })
397
+ });
398
+ const data = await response.json();
399
+ if (!response.ok) {
400
+ throw new Error(data.message || data.error || "Invalid or expired OTP code");
401
+ }
402
+ return data;
403
+ }
404
+ /**
405
+ * Resend OTP code (rate limited)
406
+ *
407
+ * @param email - User's email address
408
+ * @returns Response with success status
409
+ */
410
+ async resendOTP(email) {
411
+ const response = await fetch3(`${this.authBaseUrl}/v1/auth/otp/resend`, {
412
+ method: "POST",
413
+ headers: { "Content-Type": "application/json" },
414
+ body: JSON.stringify({
415
+ email: email.trim().toLowerCase(),
416
+ type: "email",
417
+ platform: this.platform
418
+ })
419
+ });
420
+ const data = await response.json();
421
+ if (!response.ok) {
422
+ if (response.status === 429) {
423
+ throw new Error("Rate limited. Please wait before requesting another code.");
424
+ }
425
+ throw new Error(data.message || data.error || "Failed to resend OTP");
426
+ }
427
+ return data;
428
+ }
429
+ // ============================================================================
430
+ // Magic Link Flow (for web, desktop - user clicks link in email)
431
+ // ============================================================================
432
+ /**
433
+ * Request a magic link to be sent via email
434
+ * User will click the link which redirects to your callback URL
435
+ *
436
+ * @param email - User's email address
437
+ * @param redirectUri - URL to redirect to after clicking the magic link
438
+ * @returns Response with success status
439
+ */
440
+ async requestMagicLink(email, redirectUri) {
441
+ const response = await fetch3(`${this.authBaseUrl}/v1/auth/otp/send`, {
442
+ method: "POST",
443
+ headers: { "Content-Type": "application/json" },
444
+ body: JSON.stringify({
445
+ email: email.trim().toLowerCase(),
446
+ type: "magiclink",
447
+ redirect_uri: redirectUri,
448
+ platform: this.platform,
449
+ project_scope: this.projectScope
450
+ })
451
+ });
452
+ const data = await response.json();
453
+ if (!response.ok) {
454
+ throw new Error(data.message || data.error || "Failed to send magic link");
455
+ }
456
+ return data;
457
+ }
458
+ /**
459
+ * Alternative: Use the /v1/auth/magic-link endpoint (web-optimized)
460
+ * This endpoint provides better redirect handling for web apps
461
+ *
462
+ * @param email - User's email address
463
+ * @param redirectUri - URL to redirect to after authentication
464
+ * @param createUser - Whether to create a new user if email doesn't exist
465
+ */
466
+ async requestMagicLinkWeb(email, redirectUri, createUser = true) {
467
+ const response = await fetch3(`${this.authBaseUrl}/v1/auth/magic-link`, {
468
+ method: "POST",
469
+ headers: { "Content-Type": "application/json" },
470
+ body: JSON.stringify({
471
+ email: email.trim().toLowerCase(),
472
+ redirect_uri: redirectUri,
473
+ return_to: redirectUri,
474
+ // Alias for compatibility
475
+ project_scope: this.projectScope,
476
+ platform: "web",
477
+ create_user: createUser
478
+ })
479
+ });
480
+ const data = await response.json();
481
+ if (!response.ok) {
482
+ throw new Error(data.message || data.error || "Failed to send magic link");
483
+ }
484
+ return data;
485
+ }
486
+ /**
487
+ * Exchange magic link token for auth-gateway tokens
488
+ * Called after user clicks the magic link and is redirected to your callback
489
+ *
490
+ * @param supabaseAccessToken - Access token from Supabase (from URL hash/query)
491
+ * @param state - State parameter from the callback URL
492
+ * @returns Token response with redirect URL
493
+ */
494
+ async exchangeMagicLinkToken(supabaseAccessToken, state) {
495
+ const response = await fetch3(`${this.authBaseUrl}/v1/auth/magic-link/exchange`, {
496
+ method: "POST",
497
+ headers: {
498
+ "Content-Type": "application/json",
499
+ "Authorization": `Bearer ${supabaseAccessToken}`
500
+ },
501
+ body: JSON.stringify({ state })
502
+ });
503
+ const data = await response.json();
504
+ if (!response.ok) {
505
+ throw new Error(data.message || data.error || "Magic link exchange failed");
506
+ }
507
+ return data;
508
+ }
509
+ // ============================================================================
510
+ // Utility Methods
511
+ // ============================================================================
512
+ /**
513
+ * Check if an email is valid format
514
+ */
515
+ static isValidEmail(email) {
516
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
517
+ return emailRegex.test(email.trim());
518
+ }
519
+ /**
520
+ * Check if an OTP code is valid format (6 digits)
521
+ */
522
+ static isValidOTPCode(code) {
523
+ return /^\d{6}$/.test(code.trim());
524
+ }
525
+ };
526
+
527
+ // src/flows/apikey-flow.ts
528
+ import fetch4 from "cross-fetch";
529
+ var APIKeyFlow = class extends BaseOAuthFlow {
530
+ constructor(apiKey, authBaseUrl = "https://mcp.lanonasis.com") {
531
+ super({
532
+ clientId: "api-key-client",
533
+ authBaseUrl
534
+ });
535
+ this.apiKey = apiKey;
536
+ }
537
+ /**
538
+ * "Authenticate" by returning the API key as a virtual token
539
+ * The API key will be used directly in request headers
540
+ */
541
+ async authenticate() {
542
+ if (!this.apiKey || !this.apiKey.startsWith("lano_") && !this.apiKey.startsWith("vx_")) {
543
+ throw new Error(
544
+ 'Invalid API key format. Must start with "lano_" or "vx_". Please regenerate your API key from the dashboard.'
545
+ );
546
+ }
547
+ if (this.apiKey.startsWith("vx_")) {
548
+ console.warn(
549
+ '\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.'
550
+ );
551
+ }
552
+ return {
553
+ access_token: this.apiKey,
554
+ token_type: "api-key",
555
+ expires_in: 0,
556
+ // API keys don't expire
557
+ issued_at: Date.now()
558
+ };
559
+ }
560
+ /**
561
+ * API keys don't need refresh
562
+ */
563
+ async refreshToken(refreshToken) {
564
+ throw new Error("API keys do not support token refresh");
565
+ }
566
+ /**
567
+ * Optional: Validate API key by making a test request
568
+ */
569
+ async validateAPIKey() {
570
+ try {
571
+ const response = await fetch4(`${this.authBaseUrl}/api/v1/health`, {
572
+ headers: {
573
+ "x-api-key": this.apiKey
574
+ }
575
+ });
576
+ return response.ok;
577
+ } catch (error) {
578
+ console.error("API key validation failed:", error);
579
+ return false;
580
+ }
581
+ }
582
+ };
583
+
334
584
  // src/storage/token-storage.ts
335
585
  var _fs = null;
336
586
  var _path = null;
@@ -1368,66 +1618,7 @@ var ApiKeyStorageWeb = class {
1368
1618
  };
1369
1619
 
1370
1620
  // src/client/mcp-client.ts
1371
- import fetch4 from "cross-fetch";
1372
-
1373
- // src/flows/apikey-flow.ts
1374
- import fetch3 from "cross-fetch";
1375
- var APIKeyFlow = class extends BaseOAuthFlow {
1376
- constructor(apiKey, authBaseUrl = "https://mcp.lanonasis.com") {
1377
- super({
1378
- clientId: "api-key-client",
1379
- authBaseUrl
1380
- });
1381
- this.apiKey = apiKey;
1382
- }
1383
- /**
1384
- * "Authenticate" by returning the API key as a virtual token
1385
- * The API key will be used directly in request headers
1386
- */
1387
- async authenticate() {
1388
- if (!this.apiKey || !this.apiKey.startsWith("lano_") && !this.apiKey.startsWith("vx_")) {
1389
- throw new Error(
1390
- 'Invalid API key format. Must start with "lano_" or "vx_". Please regenerate your API key from the dashboard.'
1391
- );
1392
- }
1393
- if (this.apiKey.startsWith("vx_")) {
1394
- console.warn(
1395
- '\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.'
1396
- );
1397
- }
1398
- return {
1399
- access_token: this.apiKey,
1400
- token_type: "api-key",
1401
- expires_in: 0,
1402
- // API keys don't expire
1403
- issued_at: Date.now()
1404
- };
1405
- }
1406
- /**
1407
- * API keys don't need refresh
1408
- */
1409
- async refreshToken(refreshToken) {
1410
- throw new Error("API keys do not support token refresh");
1411
- }
1412
- /**
1413
- * Optional: Validate API key by making a test request
1414
- */
1415
- async validateAPIKey() {
1416
- try {
1417
- const response = await fetch3(`${this.config.authBaseUrl}/api/v1/health`, {
1418
- headers: {
1419
- "x-api-key": this.apiKey
1420
- }
1421
- });
1422
- return response.ok;
1423
- } catch (error) {
1424
- console.error("API key validation failed:", error);
1425
- return false;
1426
- }
1427
- }
1428
- };
1429
-
1430
- // src/client/mcp-client.ts
1621
+ import fetch5 from "cross-fetch";
1431
1622
  var MCPClient = class {
1432
1623
  constructor(config = {}) {
1433
1624
  // ← NEW: Track auth mode
@@ -1656,7 +1847,7 @@ var MCPClient = class {
1656
1847
  } else {
1657
1848
  headers["Authorization"] = `Bearer ${this.accessToken}`;
1658
1849
  }
1659
- const response = await fetch4(`${this.config.mcpEndpoint}/api`, {
1850
+ const response = await fetch5(`${this.config.mcpEndpoint}/api`, {
1660
1851
  method: "POST",
1661
1852
  headers,
1662
1853
  body: JSON.stringify({
@@ -1757,7 +1948,7 @@ var MCPClient = class {
1757
1948
  };
1758
1949
 
1759
1950
  // src/client/auth-gateway-client.ts
1760
- import fetch5 from "cross-fetch";
1951
+ import fetch6 from "cross-fetch";
1761
1952
  var GatewayOAuthFlow = class extends BaseOAuthFlow {
1762
1953
  async authenticate() {
1763
1954
  throw new Error("Interactive authentication is not supported in AuthGatewayClient.");
@@ -1921,7 +2112,7 @@ var AuthGatewayClient = class {
1921
2112
  return "jwt";
1922
2113
  }
1923
2114
  async requestJson(path, options) {
1924
- const response = await fetch5(`${this.authBaseUrl}${path.startsWith("/") ? path : `/${path}`}`, options);
2115
+ const response = await fetch6(`${this.authBaseUrl}${path.startsWith("/") ? path : `/${path}`}`, options);
1925
2116
  const text = await response.text();
1926
2117
  let data = null;
1927
2118
  if (text) {
@@ -1942,12 +2133,14 @@ var AuthGatewayClient = class {
1942
2133
  }
1943
2134
  };
1944
2135
  export {
2136
+ APIKeyFlow,
1945
2137
  ApiKeyStorage,
1946
2138
  ApiKeyStorageWeb,
1947
2139
  AuthGatewayClient,
1948
2140
  BaseOAuthFlow,
1949
2141
  DesktopOAuthFlow,
1950
2142
  MCPClient,
2143
+ MagicLinkFlow,
1951
2144
  TerminalOAuthFlow,
1952
2145
  TokenStorage,
1953
2146
  TokenStorageWeb
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/oauth-client",
3
- "version": "1.2.7",
3
+ "version": "1.2.8",
4
4
  "type": "module",
5
5
  "description": "OAuth and API Key authentication client for Lan Onasis MCP integration",
6
6
  "license": "MIT",