@lanonasis/cli 3.9.5 → 3.9.7

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.
@@ -190,6 +190,10 @@ export class CLIConfig {
190
190
  }
191
191
  // Enhanced Service Discovery Integration
192
192
  async discoverServices(verbose = false) {
193
+ // Honour manually configured endpoints — skip auto-discovery so we don't clobber them.
194
+ if (this.config.manualEndpointOverrides) {
195
+ return;
196
+ }
193
197
  const isTestEnvironment = process.env.NODE_ENV === 'test';
194
198
  const forceDiscovery = process.env.FORCE_SERVICE_DISCOVERY === 'true';
195
199
  if ((isTestEnvironment && !forceDiscovery) || process.env.SKIP_SERVICE_DISCOVERY === 'true') {
@@ -434,6 +438,210 @@ export class CLIConfig {
434
438
  }
435
439
  throw new Error('Auth health endpoints unreachable');
436
440
  }
441
+ getAuthVerificationEndpoints(pathname) {
442
+ const authBase = (this.config.discoveredServices?.auth_base || 'https://auth.lanonasis.com').replace(/\/$/, '');
443
+ return Array.from(new Set([
444
+ `${authBase}${pathname}`,
445
+ `https://auth.lanonasis.com${pathname}`,
446
+ `http://localhost:4000${pathname}`
447
+ ]));
448
+ }
449
+ extractAuthErrorMessage(payload) {
450
+ if (!payload || typeof payload !== 'object') {
451
+ return undefined;
452
+ }
453
+ const data = payload;
454
+ const fields = ['message', 'error', 'reason', 'code'];
455
+ for (const field of fields) {
456
+ const value = data[field];
457
+ if (typeof value === 'string' && value.trim().length > 0) {
458
+ return value;
459
+ }
460
+ }
461
+ return undefined;
462
+ }
463
+ async verifyTokenWithAuthGateway(token) {
464
+ const headers = {
465
+ 'Authorization': `Bearer ${token}`,
466
+ 'X-Project-Scope': 'lanonasis-maas'
467
+ };
468
+ let fallbackReason = 'Unable to verify token with auth gateway';
469
+ // Primary check (required by auth contract): /v1/auth/verify
470
+ for (const endpoint of this.getAuthVerificationEndpoints('/v1/auth/verify')) {
471
+ try {
472
+ const response = await axios.post(endpoint, {}, {
473
+ headers,
474
+ timeout: 5000,
475
+ proxy: false
476
+ });
477
+ const payload = response.data;
478
+ if (payload.valid === true || Boolean(payload.payload)) {
479
+ return { valid: true, method: 'token', endpoint };
480
+ }
481
+ if (payload.valid === false) {
482
+ return {
483
+ valid: false,
484
+ method: 'token',
485
+ endpoint,
486
+ reason: this.extractAuthErrorMessage(payload) || 'Token is invalid'
487
+ };
488
+ }
489
+ }
490
+ catch (error) {
491
+ const normalizedError = this.normalizeServiceError(error);
492
+ const responsePayload = normalizedError.response?.data;
493
+ const responseCode = typeof responsePayload?.code === 'string' ? responsePayload.code : undefined;
494
+ const reason = this.extractAuthErrorMessage(responsePayload) || normalizedError.message || fallbackReason;
495
+ fallbackReason = reason;
496
+ // If auth gateway explicitly rejected token, stop early.
497
+ if ((normalizedError.response?.status === 401 || normalizedError.response?.status === 403) &&
498
+ responseCode &&
499
+ responseCode !== 'AUTH_TOKEN_MISSING') {
500
+ return { valid: false, method: 'token', endpoint, reason };
501
+ }
502
+ }
503
+ }
504
+ // Fallback for deployments where proxy layers strip Authorization headers.
505
+ for (const endpoint of this.getAuthVerificationEndpoints('/v1/auth/verify-token')) {
506
+ try {
507
+ const response = await axios.post(endpoint, { token }, {
508
+ headers: {
509
+ 'Content-Type': 'application/json',
510
+ 'X-Project-Scope': 'lanonasis-maas'
511
+ },
512
+ timeout: 5000,
513
+ proxy: false
514
+ });
515
+ const payload = response.data;
516
+ if (payload.valid === true) {
517
+ return { valid: true, method: 'token', endpoint };
518
+ }
519
+ if (payload.valid === false) {
520
+ return {
521
+ valid: false,
522
+ method: 'token',
523
+ endpoint,
524
+ reason: this.extractAuthErrorMessage(payload) || 'Token is invalid'
525
+ };
526
+ }
527
+ }
528
+ catch (error) {
529
+ const normalizedError = this.normalizeServiceError(error);
530
+ const responsePayload = normalizedError.response?.data;
531
+ fallbackReason = this.extractAuthErrorMessage(responsePayload) || normalizedError.message || fallbackReason;
532
+ }
533
+ }
534
+ return {
535
+ valid: false,
536
+ method: 'token',
537
+ reason: fallbackReason
538
+ };
539
+ }
540
+ async verifyVendorKeyWithAuthGateway(vendorKey) {
541
+ // Detect whether the stored "vendor key" is actually an OAuth/JWT access token
542
+ // (3-part base64url string separated by dots). OAuth tokens must be verified via the
543
+ // Bearer token path (/v1/auth/verify-token), not as API keys (/v1/auth/verify-api-key),
544
+ // because they are not stored in the api_keys table.
545
+ const isJwtFormat = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(vendorKey.trim());
546
+ if (isJwtFormat) {
547
+ // Delegate to token verification — the auth gateway's UAI router accepts Bearer tokens
548
+ // on /v1/auth/verify (requireAuth) and /v1/auth/verify-token (public, body-based).
549
+ return this.verifyTokenWithAuthGateway(vendorKey);
550
+ }
551
+ const headers = {
552
+ 'X-API-Key': vendorKey,
553
+ 'X-Auth-Method': 'vendor_key',
554
+ 'X-Project-Scope': 'lanonasis-maas'
555
+ };
556
+ let fallbackReason = 'Unable to verify API key with auth gateway';
557
+ // Primary check (required by auth contract): /v1/auth/verify
558
+ for (const endpoint of this.getAuthVerificationEndpoints('/v1/auth/verify')) {
559
+ try {
560
+ const response = await axios.post(endpoint, {}, {
561
+ headers,
562
+ timeout: 5000,
563
+ proxy: false
564
+ });
565
+ const payload = response.data;
566
+ if (payload.valid === true || Boolean(payload.payload)) {
567
+ return { valid: true, method: 'vendor_key', endpoint };
568
+ }
569
+ if (payload.valid === false) {
570
+ return {
571
+ valid: false,
572
+ method: 'vendor_key',
573
+ endpoint,
574
+ reason: this.extractAuthErrorMessage(payload) || 'API key is invalid'
575
+ };
576
+ }
577
+ }
578
+ catch (error) {
579
+ const normalizedError = this.normalizeServiceError(error);
580
+ const responsePayload = normalizedError.response?.data;
581
+ const responseCode = typeof responsePayload?.code === 'string' ? responsePayload.code : undefined;
582
+ const reason = this.extractAuthErrorMessage(responsePayload) || normalizedError.message || fallbackReason;
583
+ fallbackReason = reason;
584
+ // If auth gateway explicitly rejected API key, stop early.
585
+ if ((normalizedError.response?.status === 401 || normalizedError.response?.status === 403) &&
586
+ responseCode &&
587
+ responseCode !== 'AUTH_TOKEN_MISSING') {
588
+ return { valid: false, method: 'vendor_key', endpoint, reason };
589
+ }
590
+ }
591
+ }
592
+ // Fallback for deployments where reverse proxies don't forward custom auth headers on /verify.
593
+ for (const endpoint of this.getAuthVerificationEndpoints('/v1/auth/verify-api-key')) {
594
+ try {
595
+ const response = await axios.post(endpoint, {}, {
596
+ headers,
597
+ timeout: 5000,
598
+ proxy: false
599
+ });
600
+ const payload = response.data;
601
+ if (payload.valid === true) {
602
+ return { valid: true, method: 'vendor_key', endpoint };
603
+ }
604
+ if (payload.valid === false) {
605
+ return {
606
+ valid: false,
607
+ method: 'vendor_key',
608
+ endpoint,
609
+ reason: this.extractAuthErrorMessage(payload) || 'API key is invalid'
610
+ };
611
+ }
612
+ }
613
+ catch (error) {
614
+ const normalizedError = this.normalizeServiceError(error);
615
+ const responsePayload = normalizedError.response?.data;
616
+ fallbackReason = this.extractAuthErrorMessage(responsePayload) || normalizedError.message || fallbackReason;
617
+ }
618
+ }
619
+ return {
620
+ valid: false,
621
+ method: 'vendor_key',
622
+ reason: fallbackReason
623
+ };
624
+ }
625
+ async verifyCurrentCredentialsWithServer() {
626
+ await this.refreshTokenIfNeeded();
627
+ await this.discoverServices();
628
+ const token = this.getToken();
629
+ const vendorKey = await this.getVendorKeyAsync();
630
+ if (this.config.authMethod === 'vendor_key' && vendorKey) {
631
+ return this.verifyVendorKeyWithAuthGateway(vendorKey);
632
+ }
633
+ if (token) {
634
+ return this.verifyTokenWithAuthGateway(token);
635
+ }
636
+ if (vendorKey) {
637
+ return this.verifyVendorKeyWithAuthGateway(vendorKey);
638
+ }
639
+ return {
640
+ valid: false,
641
+ method: 'none',
642
+ reason: 'No credentials configured'
643
+ };
644
+ }
437
645
  // Manual endpoint override functionality
438
646
  async setManualEndpoints(endpoints) {
439
647
  if (!this.config.discoveredServices) {
@@ -461,6 +669,17 @@ export class CLIConfig {
461
669
  hasManualEndpointOverrides() {
462
670
  return !!this.config.manualEndpointOverrides;
463
671
  }
672
+ /**
673
+ * Clears the in-memory auth cache and removes the `lastValidated` timestamp.
674
+ * Called after a definitive 401 from the memory API so that the next
675
+ * `isAuthenticated()` call performs a fresh server verification rather than
676
+ * returning a stale cached result.
677
+ */
678
+ async invalidateAuthCache() {
679
+ this.authCheckCache = null;
680
+ delete this.config.lastValidated;
681
+ await this.save().catch(() => { });
682
+ }
464
683
  async clearManualEndpointOverrides() {
465
684
  delete this.config.manualEndpointOverrides;
466
685
  delete this.config.lastManualEndpointUpdate;
@@ -473,15 +692,20 @@ export class CLIConfig {
473
692
  'https://auth.lanonasis.com';
474
693
  }
475
694
  // Enhanced authentication support
476
- async setVendorKey(vendorKey) {
695
+ async setVendorKey(vendorKey, options = {}) {
477
696
  const trimmedKey = typeof vendorKey === 'string' ? vendorKey.trim() : '';
478
697
  // Minimal format validation (non-empty); rely on server-side checks for everything else
479
698
  const formatValidation = this.validateVendorKeyFormat(trimmedKey);
480
699
  if (formatValidation !== true) {
481
700
  throw new Error(typeof formatValidation === 'string' ? formatValidation : 'Vendor key is invalid');
482
701
  }
483
- // Server-side validation
484
- await this.validateVendorKeyWithServer(trimmedKey);
702
+ // Skip server-side validation when the caller already holds a valid auth credential
703
+ // (e.g. an OAuth access token being stored for MCP/API access — auth-gateway won't
704
+ // recognise it as a vendor key even though the memory API accepts it).
705
+ const isOAuthContext = ['oauth', 'oauth2'].includes(this.config.authMethod || '');
706
+ if (!options.skipServerValidation && !isOAuthContext) {
707
+ await this.validateVendorKeyWithServer(trimmedKey);
708
+ }
485
709
  // Initialize and store using ApiKeyStorage from @lanonasis/oauth-client
486
710
  // This handles encryption automatically (AES-256-GCM with machine-derived key)
487
711
  await this.apiKeyStorage.initialize();
@@ -520,16 +744,12 @@ export class CLIConfig {
520
744
  return;
521
745
  }
522
746
  try {
523
- // Import axios dynamically to avoid circular dependency
524
- // Ensure service discovery is done
525
747
  await this.discoverServices();
526
- const authBase = this.config.discoveredServices?.auth_base || 'https://auth.lanonasis.com';
527
- // Use pingAuthHealth for validation (simpler and more reliable)
528
- await this.pingAuthHealth(axios, authBase, {
529
- 'X-API-Key': vendorKey,
530
- 'X-Auth-Method': 'vendor_key',
531
- 'X-Project-Scope': 'lanonasis-maas'
532
- }, { timeout: 10000, proxy: false });
748
+ const verification = await this.verifyVendorKeyWithAuthGateway(vendorKey);
749
+ if (verification.valid) {
750
+ return;
751
+ }
752
+ throw new Error(verification.reason || 'Authentication failed. The key may be invalid, expired, or revoked.');
533
753
  }
534
754
  catch (error) {
535
755
  const normalizedError = this.normalizeServiceError(error);
@@ -687,36 +907,40 @@ export class CLIConfig {
687
907
  const vendorKey = await this.getVendorKeyAsync();
688
908
  if (!vendorKey)
689
909
  return false;
690
- // Check cache first
910
+ // Check in-memory cache first (5-minute TTL)
691
911
  if (this.authCheckCache && (Date.now() - this.authCheckCache.timestamp) < this.AUTH_CACHE_TTL) {
692
912
  return this.authCheckCache.isValid;
693
913
  }
694
- // Check if recently validated (within 24 hours)
914
+ // Track lastValidated for the offline grace period used in the catch block.
915
+ // The 24-hour skip-server-validation gate has been removed: it allowed expired/revoked
916
+ // keys to appear valid as long as they had been validated within the past day.
695
917
  const lastValidated = this.config.lastValidated;
696
- const recentlyValidated = lastValidated &&
697
- (Date.now() - new Date(lastValidated).getTime()) < (24 * 60 * 60 * 1000);
698
- if (recentlyValidated) {
699
- this.authCheckCache = { isValid: true, timestamp: Date.now() };
700
- return true;
701
- }
702
- // Vendor key not recently validated - verify with server
918
+ // Verify with server on every cache-miss
703
919
  try {
704
- await this.discoverServices();
705
- const authBase = this.config.discoveredServices?.auth_base || 'https://auth.lanonasis.com';
706
- // Ping auth health with vendor key to verify it's still valid
707
- await this.pingAuthHealth(axios, authBase, {
708
- 'X-API-Key': vendorKey,
709
- 'X-Auth-Method': 'vendor_key',
710
- 'X-Project-Scope': 'lanonasis-maas'
711
- }, { timeout: 5000, proxy: false });
920
+ const verification = await this.verifyVendorKeyWithAuthGateway(vendorKey);
921
+ if (!verification.valid) {
922
+ // Auth gateway explicitly rejected the key no grace period applies.
923
+ this.authCheckCache = { isValid: false, timestamp: Date.now() };
924
+ return false;
925
+ }
712
926
  // Update last validated timestamp on success
713
927
  this.config.lastValidated = new Date().toISOString();
714
928
  await this.save().catch(() => { }); // Don't fail auth check if save fails
715
929
  this.authCheckCache = { isValid: true, timestamp: Date.now() };
716
930
  return true;
717
931
  }
718
- catch (error) {
719
- // Server validation failed - check for grace period (7 days offline)
932
+ catch (err) {
933
+ // verifyVendorKeyWithAuthGateway throws only on network/timeout errors
934
+ // (explicit 401/403 returns {valid:false} without throwing).
935
+ // Apply the 7-day offline grace ONLY for genuine network failures.
936
+ const normalizedErr = this.normalizeServiceError(err);
937
+ const httpStatus = normalizedErr.response?.status ?? 0;
938
+ if (httpStatus === 401 || httpStatus === 403) {
939
+ // Explicit auth rejection propagated as an exception — definitely invalid.
940
+ this.authCheckCache = { isValid: false, timestamp: Date.now() };
941
+ return false;
942
+ }
943
+ // Network / server error — apply offline grace period
720
944
  const gracePeriod = 7 * 24 * 60 * 60 * 1000;
721
945
  const withinGracePeriod = lastValidated &&
722
946
  (Date.now() - new Date(lastValidated).getTime()) < gracePeriod;
@@ -727,7 +951,6 @@ export class CLIConfig {
727
951
  this.authCheckCache = { isValid: true, timestamp: Date.now() };
728
952
  return true;
729
953
  }
730
- // Grace period expired - require server validation
731
954
  if (process.env.CLI_VERBOSE === 'true') {
732
955
  console.warn('⚠️ Vendor key validation failed and grace period expired');
733
956
  }
@@ -947,22 +1170,14 @@ export class CLIConfig {
947
1170
  if (!vendorKey && !token) {
948
1171
  return false;
949
1172
  }
950
- // Import axios dynamically to avoid circular dependency
951
- // Ensure service discovery is done
952
- await this.discoverServices();
953
- const authBase = this.config.discoveredServices?.auth_base || 'https://auth.lanonasis.com';
954
- const headers = {
955
- 'X-Project-Scope': 'lanonasis-maas'
956
- };
957
- if (vendorKey) {
958
- headers['X-API-Key'] = vendorKey;
959
- headers['X-Auth-Method'] = 'vendor_key';
960
- }
961
- else if (token) {
962
- headers['Authorization'] = `Bearer ${token}`;
963
- headers['X-Auth-Method'] = 'jwt';
1173
+ const verification = this.config.authMethod === 'vendor_key' && vendorKey
1174
+ ? await this.verifyVendorKeyWithAuthGateway(vendorKey)
1175
+ : token
1176
+ ? await this.verifyTokenWithAuthGateway(token)
1177
+ : await this.verifyVendorKeyWithAuthGateway(vendorKey);
1178
+ if (!verification.valid) {
1179
+ throw new Error(verification.reason || 'Stored credentials are invalid');
964
1180
  }
965
- await this.pingAuthHealth(axios, authBase, headers);
966
1181
  // Update last validated timestamp
967
1182
  this.config.lastValidated = new Date().toISOString();
968
1183
  await this.resetFailureCount();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.9.5",
3
+ "version": "3.9.7",
4
4
  "description": "Professional CLI for LanOnasis Memory as a Service (MaaS) with MCP support, seamless inline editing, and enterprise-grade security",
5
5
  "keywords": [
6
6
  "lanonasis",
@@ -52,37 +52,37 @@
52
52
  "CHANGELOG.md"
53
53
  ],
54
54
  "dependencies": {
55
- "@lanonasis/oauth-client": "1.2.5",
55
+ "@lanonasis/oauth-client": "2.0.0",
56
56
  "@lanonasis/security-sdk": "1.0.5",
57
- "@modelcontextprotocol/sdk": "^1.25.2",
58
- "axios": "^1.7.7",
59
- "chalk": "^5.3.0",
57
+ "@modelcontextprotocol/sdk": "^1.26.0",
58
+ "axios": "^1.13.5",
59
+ "chalk": "^5.6.2",
60
60
  "cli-progress": "^3.12.0",
61
- "cli-table3": "^0.6.3",
62
- "commander": "^12.1.0",
61
+ "cli-table3": "^0.6.5",
62
+ "commander": "^14.0.3",
63
63
  "date-fns": "^4.1.0",
64
- "dotenv": "^16.4.5",
65
- "eventsource": "^4.0.0",
66
- "inquirer": "^9.3.6",
64
+ "dotenv": "^17.3.1",
65
+ "eventsource": "^4.1.0",
66
+ "inquirer": "^13.2.5",
67
67
  "jwt-decode": "^4.0.0",
68
- "open": "^10.2.0",
69
- "ora": "^8.0.1",
68
+ "open": "^11.0.0",
69
+ "ora": "^9.3.0",
70
70
  "table": "^6.9.0",
71
71
  "word-wrap": "^1.2.5",
72
- "ws": "^8.18.0",
73
- "zod": "^3.24.4"
72
+ "ws": "^8.19.0",
73
+ "zod": "^4.3.6"
74
74
  },
75
75
  "devDependencies": {
76
- "@jest/globals": "^29.7.0",
76
+ "@jest/globals": "^30.2.0",
77
77
  "@types/cli-progress": "^3.11.6",
78
- "@types/inquirer": "^9.0.7",
79
- "@types/node": "^22.19.3",
80
- "@types/ws": "^8.5.12",
81
- "fast-check": "^3.15.1",
82
- "jest": "^29.7.0",
83
- "rimraf": "^5.0.7",
84
- "ts-jest": "^29.1.1",
85
- "typescript": "^5.7.2"
78
+ "@types/inquirer": "^9.0.9",
79
+ "@types/node": "^25.3.0",
80
+ "@types/ws": "^8.18.1",
81
+ "fast-check": "^4.5.3",
82
+ "jest": "^30.2.0",
83
+ "rimraf": "^6.1.3",
84
+ "ts-jest": "^29.4.6",
85
+ "typescript": "^5.9.3"
86
86
  },
87
87
  "scripts": {
88
88
  "build": "rimraf dist && tsc -p tsconfig.json",