@lanonasis/cli 3.9.6 → 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.
@@ -22,7 +22,7 @@ export const MemoryCreateSchema = z.object({
22
22
  .uuid()
23
23
  .optional()
24
24
  .describe("Optional topic ID for organization"),
25
- metadata: z.record(z.any())
25
+ metadata: z.record(z.string(), z.any())
26
26
  .optional()
27
27
  .describe("Additional metadata")
28
28
  });
@@ -71,7 +71,7 @@ export const MemoryUpdateSchema = z.object({
71
71
  tags: z.array(z.string())
72
72
  .optional()
73
73
  .describe("New tags (replaces existing)"),
74
- metadata: z.record(z.any())
74
+ metadata: z.record(z.string(), z.any())
75
75
  .optional()
76
76
  .describe("New metadata (merges with existing)")
77
77
  });
@@ -229,7 +229,7 @@ export const BulkOperationSchema = z.object({
229
229
  .describe("Bulk operation type"),
230
230
  entity_type: z.enum(["memory", "topic", "apikey"])
231
231
  .describe("Entity type for bulk operation"),
232
- items: z.array(z.record(z.any()))
232
+ items: z.array(z.record(z.string(), z.any()))
233
233
  .min(1)
234
234
  .max(100)
235
235
  .describe("Items for bulk operation"),
@@ -249,7 +249,7 @@ export const ImportExportSchema = z.object({
249
249
  file_path: z.string()
250
250
  .optional()
251
251
  .describe("File path for import/export"),
252
- filters: z.record(z.any())
252
+ filters: z.record(z.string(), z.any())
253
253
  .optional()
254
254
  .describe("Filters for export")
255
255
  });
@@ -257,7 +257,7 @@ export const ImportExportSchema = z.object({
257
257
  export const ToolExecutionSchema = z.object({
258
258
  tool_name: z.string()
259
259
  .describe("Name of the tool to execute"),
260
- arguments: z.record(z.any())
260
+ arguments: z.record(z.string(), z.any())
261
261
  .describe("Tool arguments"),
262
262
  timeout: z.number()
263
263
  .positive()
@@ -308,7 +308,7 @@ export class SchemaValidator {
308
308
  }
309
309
  catch (error) {
310
310
  if (error instanceof z.ZodError) {
311
- throw new Error(`Validation error: ${error.errors
311
+ throw new Error(`Validation error: ${error.issues
312
312
  .map(e => `${e.path.join('.')}: ${e.message}`)
313
313
  .join(', ')}`);
314
314
  }
@@ -326,7 +326,7 @@ export class SchemaValidator {
326
326
  else {
327
327
  return {
328
328
  success: false,
329
- errors: result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`)
329
+ errors: result.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)
330
330
  };
331
331
  }
332
332
  }
@@ -149,9 +149,27 @@ export interface ApiErrorResponse {
149
149
  status_code: number;
150
150
  details?: Record<string, unknown>;
151
151
  }
152
+ export interface UserProfile {
153
+ id: string;
154
+ email: string;
155
+ name: string | null;
156
+ avatar_url: string | null;
157
+ role: string;
158
+ provider: string | null;
159
+ project_scope: string | null;
160
+ platform: string | null;
161
+ created_at: string | null;
162
+ last_sign_in_at: string | null;
163
+ metadata?: {
164
+ locale: string | null;
165
+ timezone: string | null;
166
+ };
167
+ }
152
168
  export declare class APIClient {
153
169
  private client;
154
170
  private config;
171
+ /** When true, throw on 401/403 instead of printing+exiting (for callers that handle errors) */
172
+ noExit: boolean;
155
173
  private normalizeMemoryEntry;
156
174
  private shouldUseLegacyMemoryRpcFallback;
157
175
  constructor();
@@ -171,6 +189,12 @@ export declare class APIClient {
171
189
  updateTopic(id: string, data: UpdateTopicRequest): Promise<MemoryTopic>;
172
190
  deleteTopic(id: string): Promise<void>;
173
191
  getHealth(): Promise<HealthStatus>;
192
+ /**
193
+ * Fetch the current user's profile from the auth gateway (GET /v1/auth/me).
194
+ * Works for all auth methods: OAuth Bearer token, vendor key (X-API-Key), and JWT.
195
+ * The /auth/ prefix causes the request interceptor to route this to auth_base.
196
+ */
197
+ getUserProfile(): Promise<UserProfile>;
174
198
  get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
175
199
  post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
176
200
  put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
package/dist/utils/api.js CHANGED
@@ -5,6 +5,8 @@ import { CLIConfig } from './config.js';
5
5
  export class APIClient {
6
6
  client;
7
7
  config;
8
+ /** When true, throw on 401/403 instead of printing+exiting (for callers that handle errors) */
9
+ noExit = false;
8
10
  normalizeMemoryEntry(payload) {
9
11
  // API responses are inconsistent across gateways:
10
12
  // - Some return the memory entry directly
@@ -148,11 +150,21 @@ export class APIClient {
148
150
  if (error.response) {
149
151
  const { status, data } = error.response;
150
152
  if (status === 401) {
153
+ // Invalidate the local auth cache so the next isAuthenticated() call
154
+ // performs a fresh server check rather than returning a stale result.
155
+ this.config.invalidateAuthCache().catch(() => { });
156
+ if (this.noExit) {
157
+ // Caller handles the error (e.g. auth status probe) — throw so try/catch fires
158
+ return Promise.reject(error);
159
+ }
151
160
  console.error(chalk.red('✖ Authentication failed'));
152
161
  console.log(chalk.yellow('Please run:'), chalk.white('lanonasis auth login'));
153
162
  process.exit(1);
154
163
  }
155
164
  if (status === 403) {
165
+ if (this.noExit) {
166
+ return Promise.reject(error);
167
+ }
156
168
  console.error(chalk.red('✖ Permission denied'));
157
169
  if (data.message) {
158
170
  console.error(chalk.gray(data.message));
@@ -436,6 +448,15 @@ export class APIClient {
436
448
  const response = await this.client.get('/health');
437
449
  return response.data;
438
450
  }
451
+ /**
452
+ * Fetch the current user's profile from the auth gateway (GET /v1/auth/me).
453
+ * Works for all auth methods: OAuth Bearer token, vendor key (X-API-Key), and JWT.
454
+ * The /auth/ prefix causes the request interceptor to route this to auth_base.
455
+ */
456
+ async getUserProfile() {
457
+ const response = await this.client.get('/v1/auth/me');
458
+ return response.data;
459
+ }
439
460
  // Generic HTTP methods
440
461
  async get(url, config) {
441
462
  const response = await this.client.get(url, config);
@@ -83,9 +83,18 @@ export declare class CLIConfig {
83
83
  verifyCurrentCredentialsWithServer(): Promise<RemoteAuthVerification>;
84
84
  setManualEndpoints(endpoints: Partial<CLIConfigData['discoveredServices']>): Promise<void>;
85
85
  hasManualEndpointOverrides(): boolean;
86
+ /**
87
+ * Clears the in-memory auth cache and removes the `lastValidated` timestamp.
88
+ * Called after a definitive 401 from the memory API so that the next
89
+ * `isAuthenticated()` call performs a fresh server verification rather than
90
+ * returning a stale cached result.
91
+ */
92
+ invalidateAuthCache(): Promise<void>;
86
93
  clearManualEndpointOverrides(): Promise<void>;
87
94
  getDiscoveredApiUrl(): string;
88
- setVendorKey(vendorKey: string): Promise<void>;
95
+ setVendorKey(vendorKey: string, options?: {
96
+ skipServerValidation?: boolean;
97
+ }): Promise<void>;
89
98
  validateVendorKeyFormat(vendorKey: string): string | boolean;
90
99
  private validateVendorKeyWithServer;
91
100
  getVendorKey(): string | undefined;
@@ -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') {
@@ -534,6 +538,16 @@ export class CLIConfig {
534
538
  };
535
539
  }
536
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
+ }
537
551
  const headers = {
538
552
  'X-API-Key': vendorKey,
539
553
  'X-Auth-Method': 'vendor_key',
@@ -655,6 +669,17 @@ export class CLIConfig {
655
669
  hasManualEndpointOverrides() {
656
670
  return !!this.config.manualEndpointOverrides;
657
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
+ }
658
683
  async clearManualEndpointOverrides() {
659
684
  delete this.config.manualEndpointOverrides;
660
685
  delete this.config.lastManualEndpointUpdate;
@@ -667,15 +692,20 @@ export class CLIConfig {
667
692
  'https://auth.lanonasis.com';
668
693
  }
669
694
  // Enhanced authentication support
670
- async setVendorKey(vendorKey) {
695
+ async setVendorKey(vendorKey, options = {}) {
671
696
  const trimmedKey = typeof vendorKey === 'string' ? vendorKey.trim() : '';
672
697
  // Minimal format validation (non-empty); rely on server-side checks for everything else
673
698
  const formatValidation = this.validateVendorKeyFormat(trimmedKey);
674
699
  if (formatValidation !== true) {
675
700
  throw new Error(typeof formatValidation === 'string' ? formatValidation : 'Vendor key is invalid');
676
701
  }
677
- // Server-side validation
678
- 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
+ }
679
709
  // Initialize and store using ApiKeyStorage from @lanonasis/oauth-client
680
710
  // This handles encryption automatically (AES-256-GCM with machine-derived key)
681
711
  await this.apiKeyStorage.initialize();
@@ -877,23 +907,21 @@ export class CLIConfig {
877
907
  const vendorKey = await this.getVendorKeyAsync();
878
908
  if (!vendorKey)
879
909
  return false;
880
- // Check cache first
910
+ // Check in-memory cache first (5-minute TTL)
881
911
  if (this.authCheckCache && (Date.now() - this.authCheckCache.timestamp) < this.AUTH_CACHE_TTL) {
882
912
  return this.authCheckCache.isValid;
883
913
  }
884
- // 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.
885
917
  const lastValidated = this.config.lastValidated;
886
- const recentlyValidated = lastValidated &&
887
- (Date.now() - new Date(lastValidated).getTime()) < (24 * 60 * 60 * 1000);
888
- if (recentlyValidated) {
889
- this.authCheckCache = { isValid: true, timestamp: Date.now() };
890
- return true;
891
- }
892
- // Vendor key not recently validated - verify with server
918
+ // Verify with server on every cache-miss
893
919
  try {
894
920
  const verification = await this.verifyVendorKeyWithAuthGateway(vendorKey);
895
921
  if (!verification.valid) {
896
- throw new Error(verification.reason || 'Vendor key validation failed');
922
+ // Auth gateway explicitly rejected the key no grace period applies.
923
+ this.authCheckCache = { isValid: false, timestamp: Date.now() };
924
+ return false;
897
925
  }
898
926
  // Update last validated timestamp on success
899
927
  this.config.lastValidated = new Date().toISOString();
@@ -901,8 +929,18 @@ export class CLIConfig {
901
929
  this.authCheckCache = { isValid: true, timestamp: Date.now() };
902
930
  return true;
903
931
  }
904
- catch {
905
- // 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
906
944
  const gracePeriod = 7 * 24 * 60 * 60 * 1000;
907
945
  const withinGracePeriod = lastValidated &&
908
946
  (Date.now() - new Date(lastValidated).getTime()) < gracePeriod;
@@ -913,7 +951,6 @@ export class CLIConfig {
913
951
  this.authCheckCache = { isValid: true, timestamp: Date.now() };
914
952
  return true;
915
953
  }
916
- // Grace period expired - require server validation
917
954
  if (process.env.CLI_VERBOSE === 'true') {
918
955
  console.warn('⚠️ Vendor key validation failed and grace period expired');
919
956
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.9.6",
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": "^27.5.1",
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": "^25.0.0",
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
83
  "rimraf": "^6.1.3",
84
- "ts-jest": "^29.1.1",
85
- "typescript": "^5.7.2"
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",