@lanonasis/cli 3.9.5 → 3.9.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog - @lanonasis/cli
2
2
 
3
+ ## [3.9.6] - 2026-02-21
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - **Reliable Memory Auth Routing**: Memory CRUD and search operations now consistently route through the API gateway (`https://api.lanonasis.com`) to avoid MCP endpoint contract mismatches.
8
+ - **Legacy Endpoint Compatibility**: Added fallback support for deployments that still expose RPC-style memory routes (`/api/v1/memory/*`) when REST routes return `400/405`.
9
+ - **Auth Status Accuracy**: `status` now validates live auth state against the auth verify endpoint before reporting authenticated session state.
10
+ - **OAuth Session Stability**: Requests proactively refresh OAuth/JWT sessions to reduce intermittent `memory login required` errors during long-running CLI usage.
11
+ - **Response Normalization**: Memory get/list/search handlers normalize wrapped gateway responses (`{ data: ... }`) for consistent CLI behavior across environments.
12
+
13
+ ### 📚 Documentation
14
+
15
+ - Clarified auth flow behavior for vendor keys and bearer tokens.
16
+ - Added release notes for endpoint override guidance and memory transport behavior.
17
+
3
18
  ## [3.9.3] - 2026-02-02
4
19
 
5
20
  ### ✨ Features
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # @lanonasis/cli v3.9.3 - Enterprise Security & Professional UX
1
+ # @lanonasis/cli v3.9.6 - Auth Routing & Memory Reliability
2
2
 
3
3
  [![NPM Version](https://img.shields.io/npm/v/@lanonasis/cli)](https://www.npmjs.com/package/@lanonasis/cli)
4
4
  [![Downloads](https://img.shields.io/npm/dt/@lanonasis/cli)](https://www.npmjs.com/package/@lanonasis/cli)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Golden Contract](https://img.shields.io/badge/Onasis--Core-v0.1%20Compliant-gold)](https://api.lanonasis.com/.well-known/onasis.json)
7
7
 
8
- 🎉 **NEW IN v3.9.3**: Fixed JWT authentication routing for username/password login, resolved frozen terminal during interactive input, and added non-interactive vendor key authentication (`-k` flag). Professional CLI UX with seamless inline text editing, intelligent MCP connection management, and first-run onboarding.
8
+ 🎉 **NEW IN v3.9.6**: Fixed memory auth routing to the API gateway, added compatibility fallbacks for legacy memory endpoints, improved auth status verification, and stabilized OAuth/JWT refresh behavior during CLI memory operations.
9
9
 
10
10
  ## 🚀 Quick Start
11
11
 
@@ -129,23 +129,19 @@ maas memory list
129
129
 
130
130
  ## 🔐 Security & Authentication
131
131
 
132
- ### Enterprise-Grade SHA-256 Security (v3.7.0+)
132
+ ### Enterprise-Grade API Key Handling
133
133
 
134
- All API keys are now secured with SHA-256 cryptographic hashing:
134
+ The CLI uses secure local storage and sends credentials in the expected wire format:
135
135
 
136
- - ✅ **Automatic Hash Normalization**: Keys are automatically hashed before transmission
137
- - ✅ **Double-Hash Prevention**: Smart detection prevents re-hashing already hashed keys
138
- - ✅ **Cross-Platform Compatibility**: Works seamlessly across Node.js and browser environments
139
- - ✅ **Zero Configuration**: Security is automatic and transparent
140
-
141
- ```typescript
142
- // Hash utilities are built-in and automatic
143
- // Your vendor keys are automatically secured
144
- onasis login --vendor-key pk_xxxxx.sk_xxxxx // ✅ Automatically hashed
145
- ```
136
+ - ✅ **Encrypted At Rest**: Vendor keys are stored in encrypted local storage (keytar when available, encrypted file fallback otherwise).
137
+ - ✅ **Correct On-Wire Format**: Vendor key auth sends the raw vendor key in `X-API-Key` over HTTPS.
138
+ - ✅ **Single Server-Side Hash Validation**: The API validates keys with server-side hashing and does not require client-side hashing.
139
+ - ✅ **Token-First Sessions**: OAuth/JWT sessions use `Authorization: Bearer <token>` and refresh automatically before expiry.
146
140
 
147
141
  ### Authentication Methods
148
142
 
143
+ > Transport note: for memory commands, keep `manualEndpointOverrides=false` so requests route through `https://api.lanonasis.com`.
144
+
149
145
  ### 1. Vendor Key Authentication (Recommended)
150
146
 
151
147
  Best for API integrations and automation. Copy the vendor key value exactly as shown in your LanOnasis dashboard (keys may vary in format):
@@ -553,18 +553,30 @@ program
553
553
  .action(async () => {
554
554
  // Initialize config first
555
555
  await cliConfig.init();
556
- const isAuth = await cliConfig.isAuthenticated();
556
+ const verification = await cliConfig.verifyCurrentCredentialsWithServer().catch((error) => ({
557
+ valid: false,
558
+ method: 'none',
559
+ endpoint: undefined,
560
+ reason: error instanceof Error ? error.message : String(error)
561
+ }));
562
+ const isAuth = verification.valid;
557
563
  const apiUrl = cliConfig.getApiUrl();
558
564
  console.log(chalk.blue.bold('MaaS CLI Status'));
559
565
  console.log(`API URL: ${apiUrl}`);
560
566
  console.log(`Authenticated: ${isAuth ? chalk.green('Yes') : chalk.red('No')}`);
567
+ if (process.env.CLI_VERBOSE === 'true' && verification.endpoint) {
568
+ console.log(`Verified via: ${verification.endpoint}`);
569
+ }
561
570
  if (isAuth) {
562
571
  const user = await cliConfig.getCurrentUser();
563
572
  if (user) {
564
573
  console.log(`User: ${user.email}`);
565
574
  console.log(`Plan: ${user.plan}`);
566
575
  }
576
+ return;
567
577
  }
578
+ console.log(chalk.yellow(`Auth check: ${verification.reason || 'Credential validation failed'}`));
579
+ console.log(chalk.yellow('Please run:'), chalk.white('lanonasis auth login'));
568
580
  });
569
581
  // Health command using the healthCheck function
570
582
  program
package/dist/index.js CHANGED
@@ -630,18 +630,30 @@ program
630
630
  .description('Show overall system status')
631
631
  .action(async () => {
632
632
  await cliConfig.init();
633
- const isAuth = await cliConfig.isAuthenticated();
633
+ const verification = await cliConfig.verifyCurrentCredentialsWithServer().catch((error) => ({
634
+ valid: false,
635
+ method: 'none',
636
+ endpoint: undefined,
637
+ reason: error instanceof Error ? error.message : String(error)
638
+ }));
639
+ const isAuth = verification.valid;
634
640
  const apiUrl = cliConfig.getApiUrl();
635
641
  console.log(chalk.blue.bold('MaaS CLI Status'));
636
642
  console.log(`API URL: ${apiUrl}`);
637
643
  console.log(`Authenticated: ${isAuth ? chalk.green('Yes') : chalk.red('No')}`);
644
+ if (process.env.CLI_VERBOSE === 'true' && verification.endpoint) {
645
+ console.log(`Verified via: ${verification.endpoint}`);
646
+ }
638
647
  if (isAuth) {
639
648
  const user = await cliConfig.getCurrentUser();
640
649
  if (user) {
641
650
  console.log(`User: ${user.email}`);
642
651
  console.log(`Plan: ${user.plan}`);
643
652
  }
653
+ return;
644
654
  }
655
+ console.log(chalk.yellow(`Auth check: ${verification.reason || 'Credential validation failed'}`));
656
+ console.log(chalk.yellow('Please run:'), chalk.white('lanonasis auth login'));
645
657
  });
646
658
  // Health command using the healthCheck function
647
659
  program
@@ -14,16 +14,16 @@ export declare const MemoryCreateSchema: z.ZodObject<{
14
14
  title?: string;
15
15
  content?: string;
16
16
  tags?: string[];
17
+ memory_type?: "context" | "reference" | "note";
17
18
  topic_id?: string;
18
19
  metadata?: Record<string, any>;
19
- memory_type?: "context" | "reference" | "note";
20
20
  }, {
21
21
  title?: string;
22
22
  content?: string;
23
23
  tags?: string[];
24
+ memory_type?: "context" | "reference" | "note";
24
25
  topic_id?: string;
25
26
  metadata?: Record<string, any>;
26
- memory_type?: "context" | "reference" | "note";
27
27
  }>;
28
28
  export declare const MemorySearchSchema: z.ZodObject<{
29
29
  query: z.ZodString;
@@ -36,16 +36,16 @@ export declare const MemorySearchSchema: z.ZodObject<{
36
36
  query?: string;
37
37
  tags?: string[];
38
38
  limit?: number;
39
+ memory_type?: "context" | "reference" | "note";
39
40
  topic_id?: string;
40
41
  threshold?: number;
41
- memory_type?: "context" | "reference" | "note";
42
42
  }, {
43
43
  query?: string;
44
44
  tags?: string[];
45
45
  limit?: number;
46
+ memory_type?: "context" | "reference" | "note";
46
47
  topic_id?: string;
47
48
  threshold?: number;
48
- memory_type?: "context" | "reference" | "note";
49
49
  }>;
50
50
  export declare const MemoryUpdateSchema: z.ZodObject<{
51
51
  memory_id: z.ZodString;
@@ -58,16 +58,16 @@ export declare const MemoryUpdateSchema: z.ZodObject<{
58
58
  title?: string;
59
59
  content?: string;
60
60
  tags?: string[];
61
+ memory_type?: "context" | "reference" | "note";
61
62
  memory_id?: string;
62
63
  metadata?: Record<string, any>;
63
- memory_type?: "context" | "reference" | "note";
64
64
  }, {
65
65
  title?: string;
66
66
  content?: string;
67
67
  tags?: string[];
68
+ memory_type?: "context" | "reference" | "note";
68
69
  memory_id?: string;
69
70
  metadata?: Record<string, any>;
70
- memory_type?: "context" | "reference" | "note";
71
71
  }>;
72
72
  export declare const MemoryDeleteSchema: z.ZodObject<{
73
73
  memory_id: z.ZodString;
@@ -90,19 +90,19 @@ export declare const MemoryListSchema: z.ZodObject<{
90
90
  }, "strip", z.ZodTypeAny, {
91
91
  tags?: string[];
92
92
  limit?: number;
93
- topic_id?: string;
94
- order?: "desc" | "asc";
95
- memory_type?: "context" | "reference" | "note";
96
93
  offset?: number;
94
+ memory_type?: "context" | "reference" | "note";
95
+ topic_id?: string;
97
96
  sort_by?: "title" | "created_at" | "updated_at";
97
+ order?: "desc" | "asc";
98
98
  }, {
99
99
  tags?: string[];
100
100
  limit?: number;
101
- topic_id?: string;
102
- order?: "desc" | "asc";
103
- memory_type?: "context" | "reference" | "note";
104
101
  offset?: number;
102
+ memory_type?: "context" | "reference" | "note";
103
+ topic_id?: string;
105
104
  sort_by?: "title" | "created_at" | "updated_at";
105
+ order?: "desc" | "asc";
106
106
  }>;
107
107
  export declare const TopicCreateSchema: z.ZodObject<{
108
108
  name: z.ZodString;
@@ -386,16 +386,16 @@ export declare const MCPSchemas: {
386
386
  title?: string;
387
387
  content?: string;
388
388
  tags?: string[];
389
+ memory_type?: "context" | "reference" | "note";
389
390
  topic_id?: string;
390
391
  metadata?: Record<string, any>;
391
- memory_type?: "context" | "reference" | "note";
392
392
  }, {
393
393
  title?: string;
394
394
  content?: string;
395
395
  tags?: string[];
396
+ memory_type?: "context" | "reference" | "note";
396
397
  topic_id?: string;
397
398
  metadata?: Record<string, any>;
398
- memory_type?: "context" | "reference" | "note";
399
399
  }>;
400
400
  search: z.ZodObject<{
401
401
  query: z.ZodString;
@@ -408,16 +408,16 @@ export declare const MCPSchemas: {
408
408
  query?: string;
409
409
  tags?: string[];
410
410
  limit?: number;
411
+ memory_type?: "context" | "reference" | "note";
411
412
  topic_id?: string;
412
413
  threshold?: number;
413
- memory_type?: "context" | "reference" | "note";
414
414
  }, {
415
415
  query?: string;
416
416
  tags?: string[];
417
417
  limit?: number;
418
+ memory_type?: "context" | "reference" | "note";
418
419
  topic_id?: string;
419
420
  threshold?: number;
420
- memory_type?: "context" | "reference" | "note";
421
421
  }>;
422
422
  update: z.ZodObject<{
423
423
  memory_id: z.ZodString;
@@ -430,16 +430,16 @@ export declare const MCPSchemas: {
430
430
  title?: string;
431
431
  content?: string;
432
432
  tags?: string[];
433
+ memory_type?: "context" | "reference" | "note";
433
434
  memory_id?: string;
434
435
  metadata?: Record<string, any>;
435
- memory_type?: "context" | "reference" | "note";
436
436
  }, {
437
437
  title?: string;
438
438
  content?: string;
439
439
  tags?: string[];
440
+ memory_type?: "context" | "reference" | "note";
440
441
  memory_id?: string;
441
442
  metadata?: Record<string, any>;
442
- memory_type?: "context" | "reference" | "note";
443
443
  }>;
444
444
  delete: z.ZodObject<{
445
445
  memory_id: z.ZodString;
@@ -462,19 +462,19 @@ export declare const MCPSchemas: {
462
462
  }, "strip", z.ZodTypeAny, {
463
463
  tags?: string[];
464
464
  limit?: number;
465
- topic_id?: string;
466
- order?: "desc" | "asc";
467
- memory_type?: "context" | "reference" | "note";
468
465
  offset?: number;
466
+ memory_type?: "context" | "reference" | "note";
467
+ topic_id?: string;
469
468
  sort_by?: "title" | "created_at" | "updated_at";
469
+ order?: "desc" | "asc";
470
470
  }, {
471
471
  tags?: string[];
472
472
  limit?: number;
473
- topic_id?: string;
474
- order?: "desc" | "asc";
475
- memory_type?: "context" | "reference" | "note";
476
473
  offset?: number;
474
+ memory_type?: "context" | "reference" | "note";
475
+ topic_id?: string;
477
476
  sort_by?: "title" | "created_at" | "updated_at";
477
+ order?: "desc" | "asc";
478
478
  }>;
479
479
  };
480
480
  topic: {
@@ -153,6 +153,7 @@ export declare class APIClient {
153
153
  private client;
154
154
  private config;
155
155
  private normalizeMemoryEntry;
156
+ private shouldUseLegacyMemoryRpcFallback;
156
157
  constructor();
157
158
  login(email: string, password: string): Promise<AuthResponse>;
158
159
  register(email: string, password: string, organizationName?: string): Promise<AuthResponse>;
package/dist/utils/api.js CHANGED
@@ -25,6 +25,21 @@ export class APIClient {
25
25
  }
26
26
  return payload;
27
27
  }
28
+ shouldUseLegacyMemoryRpcFallback(error) {
29
+ const status = error?.response?.status;
30
+ const errorData = error?.response?.data;
31
+ const message = `${errorData?.error || ''} ${errorData?.message || ''}`.toLowerCase();
32
+ if (status === 405) {
33
+ return true;
34
+ }
35
+ if (status === 400 && message.includes('memory id is required')) {
36
+ return true;
37
+ }
38
+ if (status === 400 && message.includes('method not allowed')) {
39
+ return true;
40
+ }
41
+ return false;
42
+ }
28
43
  constructor() {
29
44
  this.config = new CLIConfig();
30
45
  this.client = axios.create({
@@ -43,12 +58,14 @@ export class APIClient {
43
58
  const authMethod = this.config.get('authMethod');
44
59
  const vendorKey = await this.config.getVendorKeyAsync();
45
60
  const token = this.config.getToken();
61
+ const isMemoryEndpoint = typeof config.url === 'string' && config.url.startsWith('/api/v1/memories');
46
62
  const forceApiFromEnv = process.env.LANONASIS_FORCE_API === 'true'
47
63
  || process.env.CLI_FORCE_API === 'true'
48
64
  || process.env.ONASIS_FORCE_API === 'true';
49
65
  const forceApiFromConfig = this.config.get('forceApi') === true
50
66
  || this.config.get('connectionTransport') === 'api';
51
- const forceDirectApi = forceApiFromEnv || forceApiFromConfig;
67
+ // Memory CRUD/search endpoints should always use the API gateway path.
68
+ const forceDirectApi = forceApiFromEnv || forceApiFromConfig || isMemoryEndpoint;
52
69
  const prefersTokenAuth = Boolean(token) && (authMethod === 'jwt' || authMethod === 'oauth' || authMethod === 'oauth2');
53
70
  const useVendorKeyAuth = Boolean(vendorKey) && !prefersTokenAuth;
54
71
  // Determine the correct API base URL:
@@ -183,11 +200,91 @@ export class APIClient {
183
200
  return response.data;
184
201
  }
185
202
  catch (error) {
186
- // Backward-compatible fallback: newer API contracts may reject GET list and prefer search-only.
203
+ // Backward-compatible fallback: newer API contracts may reject GET list.
187
204
  if (error?.response?.status === 405) {
188
205
  const limit = Number(params.limit || 20);
189
206
  const page = Number(params.page || 1);
190
207
  const offset = Number(params.offset ?? Math.max(0, (page - 1) * limit));
208
+ // Preferred fallback: POST list endpoint (avoids triggering vector search for plain listings).
209
+ const listPayload = {
210
+ limit,
211
+ offset
212
+ };
213
+ if (params.memory_type) {
214
+ listPayload.memory_type = params.memory_type;
215
+ }
216
+ if (params.tags) {
217
+ listPayload.tags = Array.isArray(params.tags)
218
+ ? params.tags
219
+ : String(params.tags).split(',').map((tag) => tag.trim()).filter(Boolean);
220
+ }
221
+ if (params.topic_id) {
222
+ listPayload.topic_id = params.topic_id;
223
+ }
224
+ if (params.user_id) {
225
+ listPayload.user_id = params.user_id;
226
+ }
227
+ if (params.sort || params.sort_by) {
228
+ listPayload.sort_by = params.sort_by || params.sort;
229
+ }
230
+ if (params.order || params.sort_order) {
231
+ listPayload.sort_order = params.sort_order || params.order;
232
+ }
233
+ for (const endpoint of ['/api/v1/memories/list', '/api/v1/memory/list']) {
234
+ try {
235
+ const listResponse = await this.client.post(endpoint, listPayload);
236
+ const payload = listResponse.data || {};
237
+ const resultsArray = Array.isArray(payload.data)
238
+ ? payload.data
239
+ : Array.isArray(payload.memories)
240
+ ? payload.memories
241
+ : Array.isArray(payload.results)
242
+ ? payload.results
243
+ : [];
244
+ const memories = resultsArray.map((entry) => this.normalizeMemoryEntry(entry));
245
+ const pagination = (payload.pagination && typeof payload.pagination === 'object')
246
+ ? payload.pagination
247
+ : {};
248
+ const total = Number.isFinite(Number(pagination.total))
249
+ ? Number(pagination.total)
250
+ : Number.isFinite(Number(payload.total))
251
+ ? Number(payload.total)
252
+ : memories.length;
253
+ const pages = Number.isFinite(Number(pagination.total_pages))
254
+ ? Number(pagination.total_pages)
255
+ : Number.isFinite(Number(pagination.pages))
256
+ ? Number(pagination.pages)
257
+ : Math.max(1, Math.ceil(total / limit));
258
+ const currentPage = Number.isFinite(Number(pagination.page))
259
+ ? Number(pagination.page)
260
+ : Math.max(1, Math.floor(offset / limit) + 1);
261
+ const hasMore = typeof pagination.has_more === 'boolean'
262
+ ? pagination.has_more
263
+ : typeof pagination.has_next === 'boolean'
264
+ ? pagination.has_next
265
+ : (offset + memories.length) < total;
266
+ return {
267
+ ...payload,
268
+ data: memories,
269
+ memories,
270
+ pagination: {
271
+ total,
272
+ limit,
273
+ offset,
274
+ has_more: hasMore,
275
+ page: currentPage,
276
+ pages
277
+ }
278
+ };
279
+ }
280
+ catch (listError) {
281
+ if (listError?.response?.status === 404 || listError?.response?.status === 405) {
282
+ continue;
283
+ }
284
+ throw listError;
285
+ }
286
+ }
287
+ // Secondary fallback: search endpoint for legacy contracts that expose only search.
191
288
  const searchPayload = {
192
289
  query: '*',
193
290
  limit,
@@ -251,15 +348,51 @@ export class APIClient {
251
348
  }
252
349
  }
253
350
  async getMemory(id) {
254
- const response = await this.client.get(`/api/v1/memories/${id}`);
255
- return this.normalizeMemoryEntry(response.data);
351
+ try {
352
+ const response = await this.client.get(`/api/v1/memories/${id}`);
353
+ return this.normalizeMemoryEntry(response.data);
354
+ }
355
+ catch (error) {
356
+ if (this.shouldUseLegacyMemoryRpcFallback(error)) {
357
+ const fallback = await this.client.post('/api/v1/memory/get', { id });
358
+ const payload = fallback.data && typeof fallback.data === 'object'
359
+ ? fallback.data.data ?? fallback.data
360
+ : fallback.data;
361
+ return this.normalizeMemoryEntry(payload);
362
+ }
363
+ throw error;
364
+ }
256
365
  }
257
366
  async updateMemory(id, data) {
258
- const response = await this.client.put(`/api/v1/memories/${id}`, data);
259
- return this.normalizeMemoryEntry(response.data);
367
+ try {
368
+ const response = await this.client.put(`/api/v1/memories/${id}`, data);
369
+ return this.normalizeMemoryEntry(response.data);
370
+ }
371
+ catch (error) {
372
+ if (this.shouldUseLegacyMemoryRpcFallback(error)) {
373
+ const fallback = await this.client.post('/api/v1/memory/update', {
374
+ id,
375
+ ...data
376
+ });
377
+ const payload = fallback.data && typeof fallback.data === 'object'
378
+ ? fallback.data.data ?? fallback.data
379
+ : fallback.data;
380
+ return this.normalizeMemoryEntry(payload);
381
+ }
382
+ throw error;
383
+ }
260
384
  }
261
385
  async deleteMemory(id) {
262
- await this.client.delete(`/api/v1/memories/${id}`);
386
+ try {
387
+ await this.client.delete(`/api/v1/memories/${id}`);
388
+ }
389
+ catch (error) {
390
+ if (this.shouldUseLegacyMemoryRpcFallback(error)) {
391
+ await this.client.post('/api/v1/memory/delete', { id });
392
+ return;
393
+ }
394
+ throw error;
395
+ }
263
396
  }
264
397
  async searchMemories(query, options = {}) {
265
398
  const response = await this.client.post('/api/v1/memories/search', {
@@ -34,6 +34,12 @@ interface CLIConfigData {
34
34
  lastAuthFailure?: string | undefined;
35
35
  [key: string]: unknown;
36
36
  }
37
+ export type RemoteAuthVerification = {
38
+ valid: boolean;
39
+ method: 'token' | 'vendor_key' | 'none';
40
+ endpoint?: string;
41
+ reason?: string;
42
+ };
37
43
  export declare class CLIConfig {
38
44
  private configDir;
39
45
  private configPath;
@@ -70,6 +76,11 @@ export declare class CLIConfig {
70
76
  private resolveFallbackEndpoints;
71
77
  private logFallbackUsage;
72
78
  private pingAuthHealth;
79
+ private getAuthVerificationEndpoints;
80
+ private extractAuthErrorMessage;
81
+ private verifyTokenWithAuthGateway;
82
+ private verifyVendorKeyWithAuthGateway;
83
+ verifyCurrentCredentialsWithServer(): Promise<RemoteAuthVerification>;
73
84
  setManualEndpoints(endpoints: Partial<CLIConfigData['discoveredServices']>): Promise<void>;
74
85
  hasManualEndpointOverrides(): boolean;
75
86
  clearManualEndpointOverrides(): Promise<void>;
@@ -434,6 +434,200 @@ export class CLIConfig {
434
434
  }
435
435
  throw new Error('Auth health endpoints unreachable');
436
436
  }
437
+ getAuthVerificationEndpoints(pathname) {
438
+ const authBase = (this.config.discoveredServices?.auth_base || 'https://auth.lanonasis.com').replace(/\/$/, '');
439
+ return Array.from(new Set([
440
+ `${authBase}${pathname}`,
441
+ `https://auth.lanonasis.com${pathname}`,
442
+ `http://localhost:4000${pathname}`
443
+ ]));
444
+ }
445
+ extractAuthErrorMessage(payload) {
446
+ if (!payload || typeof payload !== 'object') {
447
+ return undefined;
448
+ }
449
+ const data = payload;
450
+ const fields = ['message', 'error', 'reason', 'code'];
451
+ for (const field of fields) {
452
+ const value = data[field];
453
+ if (typeof value === 'string' && value.trim().length > 0) {
454
+ return value;
455
+ }
456
+ }
457
+ return undefined;
458
+ }
459
+ async verifyTokenWithAuthGateway(token) {
460
+ const headers = {
461
+ 'Authorization': `Bearer ${token}`,
462
+ 'X-Project-Scope': 'lanonasis-maas'
463
+ };
464
+ let fallbackReason = 'Unable to verify token with auth gateway';
465
+ // Primary check (required by auth contract): /v1/auth/verify
466
+ for (const endpoint of this.getAuthVerificationEndpoints('/v1/auth/verify')) {
467
+ try {
468
+ const response = await axios.post(endpoint, {}, {
469
+ headers,
470
+ timeout: 5000,
471
+ proxy: false
472
+ });
473
+ const payload = response.data;
474
+ if (payload.valid === true || Boolean(payload.payload)) {
475
+ return { valid: true, method: 'token', endpoint };
476
+ }
477
+ if (payload.valid === false) {
478
+ return {
479
+ valid: false,
480
+ method: 'token',
481
+ endpoint,
482
+ reason: this.extractAuthErrorMessage(payload) || 'Token is invalid'
483
+ };
484
+ }
485
+ }
486
+ catch (error) {
487
+ const normalizedError = this.normalizeServiceError(error);
488
+ const responsePayload = normalizedError.response?.data;
489
+ const responseCode = typeof responsePayload?.code === 'string' ? responsePayload.code : undefined;
490
+ const reason = this.extractAuthErrorMessage(responsePayload) || normalizedError.message || fallbackReason;
491
+ fallbackReason = reason;
492
+ // If auth gateway explicitly rejected token, stop early.
493
+ if ((normalizedError.response?.status === 401 || normalizedError.response?.status === 403) &&
494
+ responseCode &&
495
+ responseCode !== 'AUTH_TOKEN_MISSING') {
496
+ return { valid: false, method: 'token', endpoint, reason };
497
+ }
498
+ }
499
+ }
500
+ // Fallback for deployments where proxy layers strip Authorization headers.
501
+ for (const endpoint of this.getAuthVerificationEndpoints('/v1/auth/verify-token')) {
502
+ try {
503
+ const response = await axios.post(endpoint, { token }, {
504
+ headers: {
505
+ 'Content-Type': 'application/json',
506
+ 'X-Project-Scope': 'lanonasis-maas'
507
+ },
508
+ timeout: 5000,
509
+ proxy: false
510
+ });
511
+ const payload = response.data;
512
+ if (payload.valid === true) {
513
+ return { valid: true, method: 'token', endpoint };
514
+ }
515
+ if (payload.valid === false) {
516
+ return {
517
+ valid: false,
518
+ method: 'token',
519
+ endpoint,
520
+ reason: this.extractAuthErrorMessage(payload) || 'Token is invalid'
521
+ };
522
+ }
523
+ }
524
+ catch (error) {
525
+ const normalizedError = this.normalizeServiceError(error);
526
+ const responsePayload = normalizedError.response?.data;
527
+ fallbackReason = this.extractAuthErrorMessage(responsePayload) || normalizedError.message || fallbackReason;
528
+ }
529
+ }
530
+ return {
531
+ valid: false,
532
+ method: 'token',
533
+ reason: fallbackReason
534
+ };
535
+ }
536
+ async verifyVendorKeyWithAuthGateway(vendorKey) {
537
+ const headers = {
538
+ 'X-API-Key': vendorKey,
539
+ 'X-Auth-Method': 'vendor_key',
540
+ 'X-Project-Scope': 'lanonasis-maas'
541
+ };
542
+ let fallbackReason = 'Unable to verify API key with auth gateway';
543
+ // Primary check (required by auth contract): /v1/auth/verify
544
+ for (const endpoint of this.getAuthVerificationEndpoints('/v1/auth/verify')) {
545
+ try {
546
+ const response = await axios.post(endpoint, {}, {
547
+ headers,
548
+ timeout: 5000,
549
+ proxy: false
550
+ });
551
+ const payload = response.data;
552
+ if (payload.valid === true || Boolean(payload.payload)) {
553
+ return { valid: true, method: 'vendor_key', endpoint };
554
+ }
555
+ if (payload.valid === false) {
556
+ return {
557
+ valid: false,
558
+ method: 'vendor_key',
559
+ endpoint,
560
+ reason: this.extractAuthErrorMessage(payload) || 'API key is invalid'
561
+ };
562
+ }
563
+ }
564
+ catch (error) {
565
+ const normalizedError = this.normalizeServiceError(error);
566
+ const responsePayload = normalizedError.response?.data;
567
+ const responseCode = typeof responsePayload?.code === 'string' ? responsePayload.code : undefined;
568
+ const reason = this.extractAuthErrorMessage(responsePayload) || normalizedError.message || fallbackReason;
569
+ fallbackReason = reason;
570
+ // If auth gateway explicitly rejected API key, stop early.
571
+ if ((normalizedError.response?.status === 401 || normalizedError.response?.status === 403) &&
572
+ responseCode &&
573
+ responseCode !== 'AUTH_TOKEN_MISSING') {
574
+ return { valid: false, method: 'vendor_key', endpoint, reason };
575
+ }
576
+ }
577
+ }
578
+ // Fallback for deployments where reverse proxies don't forward custom auth headers on /verify.
579
+ for (const endpoint of this.getAuthVerificationEndpoints('/v1/auth/verify-api-key')) {
580
+ try {
581
+ const response = await axios.post(endpoint, {}, {
582
+ headers,
583
+ timeout: 5000,
584
+ proxy: false
585
+ });
586
+ const payload = response.data;
587
+ if (payload.valid === true) {
588
+ return { valid: true, method: 'vendor_key', endpoint };
589
+ }
590
+ if (payload.valid === false) {
591
+ return {
592
+ valid: false,
593
+ method: 'vendor_key',
594
+ endpoint,
595
+ reason: this.extractAuthErrorMessage(payload) || 'API key is invalid'
596
+ };
597
+ }
598
+ }
599
+ catch (error) {
600
+ const normalizedError = this.normalizeServiceError(error);
601
+ const responsePayload = normalizedError.response?.data;
602
+ fallbackReason = this.extractAuthErrorMessage(responsePayload) || normalizedError.message || fallbackReason;
603
+ }
604
+ }
605
+ return {
606
+ valid: false,
607
+ method: 'vendor_key',
608
+ reason: fallbackReason
609
+ };
610
+ }
611
+ async verifyCurrentCredentialsWithServer() {
612
+ await this.refreshTokenIfNeeded();
613
+ await this.discoverServices();
614
+ const token = this.getToken();
615
+ const vendorKey = await this.getVendorKeyAsync();
616
+ if (this.config.authMethod === 'vendor_key' && vendorKey) {
617
+ return this.verifyVendorKeyWithAuthGateway(vendorKey);
618
+ }
619
+ if (token) {
620
+ return this.verifyTokenWithAuthGateway(token);
621
+ }
622
+ if (vendorKey) {
623
+ return this.verifyVendorKeyWithAuthGateway(vendorKey);
624
+ }
625
+ return {
626
+ valid: false,
627
+ method: 'none',
628
+ reason: 'No credentials configured'
629
+ };
630
+ }
437
631
  // Manual endpoint override functionality
438
632
  async setManualEndpoints(endpoints) {
439
633
  if (!this.config.discoveredServices) {
@@ -520,16 +714,12 @@ export class CLIConfig {
520
714
  return;
521
715
  }
522
716
  try {
523
- // Import axios dynamically to avoid circular dependency
524
- // Ensure service discovery is done
525
717
  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 });
718
+ const verification = await this.verifyVendorKeyWithAuthGateway(vendorKey);
719
+ if (verification.valid) {
720
+ return;
721
+ }
722
+ throw new Error(verification.reason || 'Authentication failed. The key may be invalid, expired, or revoked.');
533
723
  }
534
724
  catch (error) {
535
725
  const normalizedError = this.normalizeServiceError(error);
@@ -701,21 +891,17 @@ export class CLIConfig {
701
891
  }
702
892
  // Vendor key not recently validated - verify with server
703
893
  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 });
894
+ const verification = await this.verifyVendorKeyWithAuthGateway(vendorKey);
895
+ if (!verification.valid) {
896
+ throw new Error(verification.reason || 'Vendor key validation failed');
897
+ }
712
898
  // Update last validated timestamp on success
713
899
  this.config.lastValidated = new Date().toISOString();
714
900
  await this.save().catch(() => { }); // Don't fail auth check if save fails
715
901
  this.authCheckCache = { isValid: true, timestamp: Date.now() };
716
902
  return true;
717
903
  }
718
- catch (error) {
904
+ catch {
719
905
  // Server validation failed - check for grace period (7 days offline)
720
906
  const gracePeriod = 7 * 24 * 60 * 60 * 1000;
721
907
  const withinGracePeriod = lastValidated &&
@@ -947,22 +1133,14 @@ export class CLIConfig {
947
1133
  if (!vendorKey && !token) {
948
1134
  return false;
949
1135
  }
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';
1136
+ const verification = this.config.authMethod === 'vendor_key' && vendorKey
1137
+ ? await this.verifyVendorKeyWithAuthGateway(vendorKey)
1138
+ : token
1139
+ ? await this.verifyTokenWithAuthGateway(token)
1140
+ : await this.verifyVendorKeyWithAuthGateway(vendorKey);
1141
+ if (!verification.valid) {
1142
+ throw new Error(verification.reason || 'Stored credentials are invalid');
964
1143
  }
965
- await this.pingAuthHealth(axios, authBase, headers);
966
1144
  // Update last validated timestamp
967
1145
  this.config.lastValidated = new Date().toISOString();
968
1146
  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.6",
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",
@@ -73,14 +73,14 @@
73
73
  "zod": "^3.24.4"
74
74
  },
75
75
  "devDependencies": {
76
- "@jest/globals": "^29.7.0",
76
+ "@jest/globals": "^27.5.1",
77
77
  "@types/cli-progress": "^3.11.6",
78
78
  "@types/inquirer": "^9.0.7",
79
79
  "@types/node": "^22.19.3",
80
80
  "@types/ws": "^8.5.12",
81
81
  "fast-check": "^3.15.1",
82
- "jest": "^29.7.0",
83
- "rimraf": "^5.0.7",
82
+ "jest": "^25.0.0",
83
+ "rimraf": "^6.1.3",
84
84
  "ts-jest": "^29.1.1",
85
85
  "typescript": "^5.7.2"
86
86
  },