mindsim 0.1.6 → 0.1.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.
package/src/config.ts CHANGED
@@ -1,7 +1,33 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import type { DeviceAuthConfig } from "./types";
4
+ import { getLogger } from "./logger";
5
+ import type { DeveloperUseCase, DeviceAuthConfig, MindsimScope, MindsimServiceJson } from "./types";
6
+
7
+ // =============================================================================
8
+ // Gap 7 Fix: Browser Environment Detection
9
+ // =============================================================================
10
+
11
+ /**
12
+ * Check if we're running in a Node.js environment.
13
+ * Returns false in browsers to prevent process.env access errors.
14
+ */
15
+ function isNodeEnvironment(): boolean {
16
+ return (
17
+ typeof process !== "undefined" && process.versions != null && process.versions.node != null
18
+ );
19
+ }
20
+
21
+ /**
22
+ * Safely get an environment variable.
23
+ * Returns undefined in browser environments.
24
+ */
25
+ function getEnvVar(name: string): string | undefined {
26
+ if (!isNodeEnvironment()) {
27
+ return undefined;
28
+ }
29
+ return process.env[name];
30
+ }
5
31
 
6
32
  const API_BASE_URL = "https://api.reasoner.com/api/mindsim";
7
33
  const WORKOS_DEVICE_AUTH_URL = "https://auth.reasoner.com/user_management/authorize/device";
@@ -9,34 +35,390 @@ const WORKOS_TOKEN_URL = "https://auth.reasoner.com/user_management/authenticate
9
35
  const WORKOS_CLIENT_ID = "client_01GPECHM1J9DMY7WQNKTJ195P6";
10
36
  const SDK_KEYS_API_URL = "https://api.reasoner.com/api/sdk/keys";
11
37
 
12
- export function getConfigDir() {
38
+ // Valid scopes for validation (must match rainmaker's ALL_SCOPES)
39
+ const VALID_SCOPES: MindsimScope[] = [
40
+ "minds:read",
41
+ "minds:write",
42
+ "simulate:run",
43
+ "simulate:read",
44
+ "users:read",
45
+ "users:write",
46
+ "org:admin",
47
+ ];
48
+
49
+ // Valid use cases for validation
50
+ const VALID_USE_CASES: DeveloperUseCase[] = ["internal-workflow", "customer-facing", "agentic-ai"];
51
+
52
+ // =============================================================================
53
+ // Gap 12-14 Fix: Validation Helpers
54
+ // =============================================================================
55
+
56
+ /**
57
+ * UUID v4 validation regex
58
+ */
59
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
60
+
61
+ /**
62
+ * Validate a UUID string
63
+ */
64
+ function isValidUUID(value: unknown): boolean {
65
+ return typeof value === "string" && UUID_REGEX.test(value);
66
+ }
67
+
68
+ /**
69
+ * Validate a URL string (must be HTTPS for production)
70
+ */
71
+ function isValidHttpsUrl(value: unknown): boolean {
72
+ if (typeof value !== "string") return false;
73
+ try {
74
+ const url = new URL(value);
75
+ return url.protocol === "https:";
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Validate an ISO 8601 timestamp string
83
+ */
84
+ function isValidISOTimestamp(value: unknown): boolean {
85
+ if (typeof value !== "string") return false;
86
+ const date = new Date(value);
87
+ return !Number.isNaN(date.getTime()) && value.includes("T");
88
+ }
89
+
90
+ /**
91
+ * Gap 8 Fix: Validate an email address format
92
+ */
93
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
94
+ function isValidEmail(value: unknown): boolean {
95
+ return typeof value === "string" && EMAIL_REGEX.test(value);
96
+ }
97
+
98
+ /**
99
+ * Get the MindSim config directory path.
100
+ * @returns Path to ~/.mindsim directory
101
+ */
102
+ export function getConfigDir(): string {
13
103
  return path.join(os.homedir(), ".mindsim");
14
104
  }
15
105
 
16
- export function getConfigFile() {
106
+ /**
107
+ * Get the MindSim config file path.
108
+ * @returns Path to ~/.mindsim/config file
109
+ */
110
+ export function getConfigFile(): string {
17
111
  return path.join(getConfigDir(), "config");
18
112
  }
19
113
 
114
+ // =============================================================================
115
+ // Service JSON Validation (Gap 6 Fix)
116
+ // =============================================================================
117
+
118
+ /**
119
+ * Validation result for service JSON.
120
+ */
121
+ export interface ServiceJsonValidationResult {
122
+ valid: boolean;
123
+ errors: string[];
124
+ serviceJson?: MindsimServiceJson;
125
+ }
126
+
127
+ /**
128
+ * Validate a service JSON object has all required fields with correct types.
129
+ * This provides detailed error messages for debugging.
130
+ *
131
+ * @param json - The parsed JSON object to validate
132
+ * @returns Validation result with errors if invalid
133
+ */
134
+ export function validateServiceJson(json: unknown): ServiceJsonValidationResult {
135
+ const errors: string[] = [];
136
+
137
+ if (!json || typeof json !== "object") {
138
+ return { valid: false, errors: ["Service JSON must be a valid object"] };
139
+ }
140
+
141
+ const obj = json as Record<string, unknown>;
142
+
143
+ // Required string fields
144
+ const requiredStrings: Array<{ key: string; name: string }> = [
145
+ { key: "type", name: "type" },
146
+ { key: "version", name: "version" },
147
+ { key: "project_id", name: "project_id" },
148
+ { key: "project_name", name: "project_name" },
149
+ { key: "environment", name: "environment" },
150
+ { key: "client_email", name: "client_email" },
151
+ { key: "client_id", name: "client_id" },
152
+ { key: "api_key", name: "api_key" },
153
+ { key: "api_base_url", name: "api_base_url" },
154
+ { key: "created_at", name: "created_at" },
155
+ ];
156
+
157
+ for (const { key, name } of requiredStrings) {
158
+ if (typeof obj[key] !== "string" || obj[key] === "") {
159
+ errors.push(`Missing or invalid required field: ${name}`);
160
+ }
161
+ }
162
+
163
+ // Validate type field value
164
+ if (obj.type !== "mindsim_service_account") {
165
+ errors.push('type field must be "mindsim_service_account"');
166
+ }
167
+
168
+ // Validate environment field value
169
+ if (obj.environment !== "production") {
170
+ errors.push('environment field must be "production"');
171
+ }
172
+
173
+ // Validate scopes array
174
+ if (!Array.isArray(obj.scopes)) {
175
+ errors.push("scopes must be an array");
176
+ } else if (obj.scopes.length === 0) {
177
+ errors.push("scopes array cannot be empty");
178
+ } else {
179
+ for (const scope of obj.scopes) {
180
+ if (!VALID_SCOPES.includes(scope as MindsimScope)) {
181
+ errors.push(`Invalid scope: ${scope}. Valid scopes: ${VALID_SCOPES.join(", ")}`);
182
+ }
183
+ }
184
+ }
185
+
186
+ // Validate app_metadata object
187
+ if (!obj.app_metadata || typeof obj.app_metadata !== "object") {
188
+ errors.push("app_metadata must be an object");
189
+ } else {
190
+ const metadata = obj.app_metadata as Record<string, unknown>;
191
+ const metadataFields = ["app_id", "workspace_id", "mindsim_org_id"];
192
+
193
+ for (const field of metadataFields) {
194
+ if (typeof metadata[field] !== "string" || metadata[field] === "") {
195
+ errors.push(`Missing or invalid app_metadata.${field}`);
196
+ }
197
+ }
198
+
199
+ // Validate use_case
200
+ if (!VALID_USE_CASES.includes(metadata.use_case as DeveloperUseCase)) {
201
+ errors.push(
202
+ `Invalid app_metadata.use_case: ${metadata.use_case}. Valid values: ${VALID_USE_CASES.join(", ")}`,
203
+ );
204
+ }
205
+
206
+ // Gap 12 Fix: Validate UUID format for ID fields
207
+ const uuidFields = ["app_id", "workspace_id", "mindsim_org_id"];
208
+ for (const field of uuidFields) {
209
+ if (metadata[field] && !isValidUUID(metadata[field])) {
210
+ errors.push(`Invalid UUID format for app_metadata.${field}`);
211
+ }
212
+ }
213
+ }
214
+
215
+ // Gap 12 Fix: Validate client_id as UUID
216
+ if (obj.client_id && !isValidUUID(obj.client_id)) {
217
+ errors.push("client_id must be a valid UUID");
218
+ }
219
+
220
+ // Gap 8 Fix: Validate client_email as valid email format
221
+ if (obj.client_email && !isValidEmail(obj.client_email)) {
222
+ errors.push("client_email must be a valid email address");
223
+ }
224
+
225
+ // Gap 13 Fix: Validate api_base_url as HTTPS URL
226
+ if (obj.api_base_url && !isValidHttpsUrl(obj.api_base_url)) {
227
+ errors.push("api_base_url must be a valid HTTPS URL");
228
+ }
229
+
230
+ // Gap 14 Fix: Validate timestamps as ISO 8601
231
+ if (obj.created_at && !isValidISOTimestamp(obj.created_at)) {
232
+ errors.push("created_at must be a valid ISO 8601 timestamp");
233
+ }
234
+ if (obj.expires_at && !isValidISOTimestamp(obj.expires_at)) {
235
+ errors.push("expires_at must be a valid ISO 8601 timestamp");
236
+ }
237
+ if (obj.issued_at && !isValidISOTimestamp(obj.issued_at)) {
238
+ errors.push("issued_at must be a valid ISO 8601 timestamp");
239
+ }
240
+
241
+ if (errors.length > 0) {
242
+ return { valid: false, errors };
243
+ }
244
+
245
+ return { valid: true, errors: [], serviceJson: obj as unknown as MindsimServiceJson };
246
+ }
247
+
248
+ // =============================================================================
249
+ // Service JSON Loading Functions
250
+ // =============================================================================
251
+
252
+ /**
253
+ * Load service JSON from a file path.
254
+ *
255
+ * @param filePath - Path to the service JSON file
256
+ * @returns MindsimServiceJson object or null if invalid/not found
257
+ */
258
+ export function loadServiceJsonFromFile(filePath: string): MindsimServiceJson | null {
259
+ try {
260
+ if (!fs.existsSync(filePath)) {
261
+ return null;
262
+ }
263
+ const content = fs.readFileSync(filePath, "utf-8");
264
+
265
+ let json: unknown;
266
+ try {
267
+ json = JSON.parse(content);
268
+ } catch (parseError) {
269
+ getLogger().error("Failed to parse service JSON file (invalid JSON syntax):", parseError);
270
+ return null;
271
+ }
272
+
273
+ // Validate all required fields
274
+ const validation = validateServiceJson(json);
275
+ if (!validation.valid) {
276
+ getLogger().error("Invalid service JSON:", validation.errors.join("; "));
277
+ return null;
278
+ }
279
+
280
+ return validation.serviceJson as MindsimServiceJson;
281
+ } catch (error) {
282
+ // Distinguish between different error types
283
+ if (error instanceof Error && "code" in error) {
284
+ const nodeError = error as NodeJS.ErrnoException;
285
+ if (nodeError.code === "EACCES") {
286
+ getLogger().error("Permission denied reading service JSON file:", filePath);
287
+ } else if (nodeError.code === "ENOENT") {
288
+ // File doesn't exist - this is handled above, but just in case
289
+ return null;
290
+ } else {
291
+ getLogger().error(`Failed to read service JSON file (${nodeError.code}):`, error);
292
+ }
293
+ } else {
294
+ getLogger().error("Failed to load service JSON:", error);
295
+ }
296
+ return null;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Load service JSON from a base64-encoded string.
302
+ * Useful for environment variables in containerized environments.
303
+ *
304
+ * @param base64 - Base64-encoded service JSON string
305
+ * @returns MindsimServiceJson object or null if invalid
306
+ */
307
+ export function loadServiceJsonFromBase64(base64: string): MindsimServiceJson | null {
308
+ try {
309
+ if (!base64 || base64.trim() === "") {
310
+ getLogger().error("Empty base64 string provided");
311
+ return null;
312
+ }
313
+
314
+ const decoded = Buffer.from(base64, "base64").toString("utf-8");
315
+
316
+ if (!decoded || decoded.trim() === "") {
317
+ getLogger().error("Base64 decoded to empty string");
318
+ return null;
319
+ }
320
+
321
+ let json: unknown;
322
+ try {
323
+ json = JSON.parse(decoded);
324
+ } catch (parseError) {
325
+ getLogger().error("Failed to parse decoded service JSON (invalid JSON syntax):", parseError);
326
+ return null;
327
+ }
328
+
329
+ // Validate all required fields
330
+ const validation = validateServiceJson(json);
331
+ if (!validation.valid) {
332
+ getLogger().error("Invalid service JSON:", validation.errors.join("; "));
333
+ return null;
334
+ }
335
+
336
+ return validation.serviceJson as MindsimServiceJson;
337
+ } catch (error) {
338
+ getLogger().error("Failed to decode service JSON from base64:", error);
339
+ return null;
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Load service JSON from environment variables.
345
+ * Priority: MINDSIM_SERVICE_JSON (file path) > MINDSIM_SERVICE_JSON_BASE64 (inline)
346
+ *
347
+ * @returns MindsimServiceJson object or null if not configured
348
+ */
349
+ export function loadServiceJson(): MindsimServiceJson | null {
350
+ // Gap 7 Fix: Check Node environment before accessing env vars
351
+ if (!isNodeEnvironment()) {
352
+ return null;
353
+ }
354
+
355
+ // Try file path first
356
+ const filePath = getEnvVar("MINDSIM_SERVICE_JSON");
357
+ if (filePath) {
358
+ const serviceJson = loadServiceJsonFromFile(filePath);
359
+ if (serviceJson) {
360
+ return serviceJson;
361
+ }
362
+ getLogger().warn(`MINDSIM_SERVICE_JSON set but file not found or invalid: ${filePath}`);
363
+ }
364
+
365
+ // Try base64 encoded
366
+ const base64 = getEnvVar("MINDSIM_SERVICE_JSON_BASE64");
367
+ if (base64) {
368
+ const serviceJson = loadServiceJsonFromBase64(base64);
369
+ if (serviceJson) {
370
+ return serviceJson;
371
+ }
372
+ getLogger().warn("MINDSIM_SERVICE_JSON_BASE64 set but failed to decode");
373
+ }
374
+
375
+ return null;
376
+ }
377
+
378
+ // =============================================================================
379
+ // API Key Loading
380
+ // =============================================================================
381
+
382
+ /**
383
+ * Load API key with priority:
384
+ * 1. Service JSON (env vars)
385
+ * 2. MINDSIM_API_KEY env var
386
+ * 3. Config file (~/.mindsim/config)
387
+ */
20
388
  export const loadApiKey = (): string | null => {
21
389
  try {
22
- if (process.env.MINDSIM_API_KEY) {
23
- return process.env.MINDSIM_API_KEY;
390
+ // Priority 1: Service JSON
391
+ const serviceJson = loadServiceJson();
392
+ if (serviceJson?.api_key) {
393
+ return serviceJson.api_key;
24
394
  }
25
395
 
26
- const configFile = getConfigFile();
396
+ // Priority 2: Direct API key env var
397
+ const envApiKey = getEnvVar("MINDSIM_API_KEY");
398
+ if (envApiKey) {
399
+ return envApiKey;
400
+ }
27
401
 
28
- if (fs.existsSync(configFile)) {
29
- const config = JSON.parse(fs.readFileSync(configFile, "utf-8"));
30
- return config.apiKey || null;
402
+ // Priority 3: Config file (only in Node environment)
403
+ if (isNodeEnvironment()) {
404
+ const configFile = getConfigFile();
405
+ if (fs.existsSync(configFile)) {
406
+ const config = JSON.parse(fs.readFileSync(configFile, "utf-8"));
407
+ return config.apiKey || null;
408
+ }
31
409
  }
32
410
  } catch (error) {
33
- // TODO: define logging interface so users can control where they are written to
34
- console.error(error);
411
+ getLogger().error("Failed to load API key:", error);
35
412
  }
36
413
  return null;
37
414
  };
38
415
 
39
416
  export const saveApiKey = (apiKey: string): void => {
417
+ // Gap 9 Fix: Check Node environment before accessing fs
418
+ if (!isNodeEnvironment()) {
419
+ throw new Error("saveApiKey is only available in Node.js environments");
420
+ }
421
+
40
422
  const configDir = getConfigDir();
41
423
  const configFile = getConfigFile();
42
424
 
@@ -48,13 +430,23 @@ export const saveApiKey = (apiKey: string): void => {
48
430
 
49
431
  export const getDeviceAuthConfig = (): DeviceAuthConfig => {
50
432
  return {
51
- deviceAuthUrl: process.env.MINDSIM_WORKOS_DEVICE_AUTH_URL || WORKOS_DEVICE_AUTH_URL,
52
- tokenUrl: process.env.MINDSIM_WORKOS_TOKEN_URL || WORKOS_TOKEN_URL,
53
- clientId: process.env.MINDSIM_WORKOS_CLIENT_ID || WORKOS_CLIENT_ID,
54
- sdkKeysApiUrl: process.env.MINDSIM_SDK_KEYS_API_URL || SDK_KEYS_API_URL,
433
+ deviceAuthUrl: getEnvVar("MINDSIM_WORKOS_DEVICE_AUTH_URL") || WORKOS_DEVICE_AUTH_URL,
434
+ tokenUrl: getEnvVar("MINDSIM_WORKOS_TOKEN_URL") || WORKOS_TOKEN_URL,
435
+ clientId: getEnvVar("MINDSIM_WORKOS_CLIENT_ID") || WORKOS_CLIENT_ID,
436
+ sdkKeysApiUrl: getEnvVar("MINDSIM_SDK_KEYS_API_URL") || SDK_KEYS_API_URL,
55
437
  };
56
438
  };
57
439
 
58
- export const getApiBaseUrl = () => {
59
- return process.env.MIND_SIM_API_BASE_URL || API_BASE_URL;
440
+ /**
441
+ * Get the API base URL.
442
+ * Priority: MINDSIM_API_BASE_URL > MIND_SIM_API_BASE_URL (deprecated) > default
443
+ * @returns API base URL
444
+ */
445
+ export const getApiBaseUrl = (): string => {
446
+ // Gap 12 Fix: Use consistent naming (MINDSIM_*) with backwards compatibility
447
+ return (
448
+ getEnvVar("MINDSIM_API_BASE_URL") ||
449
+ getEnvVar("MIND_SIM_API_BASE_URL") || // Deprecated, for backwards compatibility
450
+ API_BASE_URL
451
+ );
60
452
  };
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import axios, { type AxiosInstance } from "axios";
2
- import { getApiBaseUrl, loadApiKey } from "./config";
2
+ import { getApiBaseUrl, loadApiKey, loadServiceJson, validateServiceJson } from "./config";
3
3
  import { ArtifactsResource } from "./resources/artifacts";
4
4
  import { MindTopicsResource } from "./resources/mind-topics";
5
5
  import { MindsResource } from "./resources/minds";
@@ -7,10 +7,60 @@ import { PsychometricsResource } from "./resources/psychometrics";
7
7
  import { SimulationsResource } from "./resources/simulations";
8
8
  import { SnapshotsResource } from "./resources/snapshots";
9
9
  import { TagsResource } from "./resources/tags";
10
+ import type { MindsimServiceJson } from "./types";
10
11
  import { checkForUpdates, getPackageVersion } from "./version";
11
12
 
13
+ // =============================================================================
14
+ // Gap 15 Fix: Browser Compatibility Check
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Check if the SDK is running in a Node.js environment.
19
+ * Some features (like file-based service JSON loading) only work in Node.js.
20
+ */
21
+ export function isNodeEnvironment(): boolean {
22
+ return (
23
+ typeof process !== "undefined" && process.versions != null && process.versions.node != null
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Check if the SDK is running in a browser environment.
29
+ */
30
+ export function isBrowserEnvironment(): boolean {
31
+ // Use typeof checks to avoid ReferenceError in Node.js
32
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
33
+ return typeof globalThis !== "undefined" && "window" in globalThis && "document" in globalThis;
34
+ }
35
+
36
+ // =============================================================================
37
+ // Gap 14 Fix: Auth Method Detection
38
+ // =============================================================================
39
+
40
+ /**
41
+ * Authentication method used by the MindSim client.
42
+ */
43
+ export type AuthMethod =
44
+ | "service_json" // Service JSON credentials (file or base64)
45
+ | "api_key_env" // MINDSIM_API_KEY environment variable
46
+ | "api_key_constructor" // API key passed to constructor
47
+ | "config_file"; // ~/.mindsim/config file
48
+
49
+ /**
50
+ * Options for initializing the MindSim client.
51
+ */
52
+ export interface MindSimOptions {
53
+ /** Custom API base URL (overrides default and service JSON) */
54
+ apiBaseUrl?: string;
55
+ /** Service JSON credentials (alternative to API key) */
56
+ serviceJson?: MindsimServiceJson;
57
+ }
58
+
12
59
  export class MindSim {
13
60
  private client: AxiosInstance;
61
+ private serviceJson: MindsimServiceJson | null = null;
62
+ // Gap 14 Fix: Track which auth method was used
63
+ private authMethod: AuthMethod = "config_file";
14
64
 
15
65
  public artifacts: ArtifactsResource;
16
66
  public minds: MindsResource;
@@ -20,20 +70,72 @@ export class MindSim {
20
70
  public simulations: SimulationsResource;
21
71
  public tags: TagsResource;
22
72
 
23
- constructor(apiKey?: string, options?: { apiBaseUrl?: string }) {
73
+ constructor(apiKey?: string, options?: MindSimOptions) {
24
74
  // 1. Trigger the auto-update check (Fire and forget, do not await)
25
75
  checkForUpdates().catch(() => {
26
76
  // Catching here ensures no unhandled promise rejections in the user's console
27
77
  // if the check fails completely.
28
78
  });
29
79
 
30
- const token = apiKey || loadApiKey();
31
80
  const sdkVersion = getPackageVersion();
32
- const apiBaseUrl = options?.apiBaseUrl || getApiBaseUrl();
81
+
82
+ // Priority for credentials:
83
+ // 1. Constructor apiKey parameter
84
+ // 2. Constructor serviceJson option
85
+ // 3. Environment service JSON (MINDSIM_SERVICE_JSON or MINDSIM_SERVICE_JSON_BASE64)
86
+ // 4. Environment API key (MINDSIM_API_KEY)
87
+ // 5. Config file (~/.mindsim/config)
88
+
89
+ let token: string | null = apiKey || null;
90
+ let apiBaseUrl = options?.apiBaseUrl || getApiBaseUrl();
91
+
92
+ // Track auth method for Gap 14 fix
93
+ if (apiKey) {
94
+ this.authMethod = "api_key_constructor";
95
+ }
96
+
97
+ // Check for service JSON if no direct API key provided
98
+ if (!token) {
99
+ let serviceJson: MindsimServiceJson | null = null;
100
+
101
+ // Gap 3 Fix: Validate serviceJson when passed via constructor options
102
+ if (options?.serviceJson) {
103
+ const validation = validateServiceJson(options.serviceJson);
104
+ if (!validation.valid) {
105
+ throw new Error(`Invalid serviceJson: ${validation.errors.join(", ")}`);
106
+ }
107
+ serviceJson = validation.serviceJson as MindsimServiceJson;
108
+ } else {
109
+ // loadServiceJson() already validates internally
110
+ serviceJson = loadServiceJson();
111
+ }
112
+
113
+ if (serviceJson) {
114
+ this.serviceJson = serviceJson;
115
+ this.authMethod = "service_json";
116
+ token = serviceJson.api_key;
117
+ // Use API base URL from service JSON if not explicitly overridden
118
+ if (!options?.apiBaseUrl && serviceJson.api_base_url) {
119
+ apiBaseUrl = serviceJson.api_base_url;
120
+ }
121
+ }
122
+ }
123
+
124
+ // Fallback to traditional loading
125
+ if (!token) {
126
+ // Gap 7 Fix: Check if MINDSIM_API_KEY is set (only in Node environment)
127
+ const hasEnvApiKey = isNodeEnvironment() && process.env.MINDSIM_API_KEY;
128
+ if (hasEnvApiKey) {
129
+ this.authMethod = "api_key_env";
130
+ } else {
131
+ this.authMethod = "config_file";
132
+ }
133
+ token = loadApiKey();
134
+ }
33
135
 
34
136
  if (!token) {
35
137
  throw new Error(
36
- "API Key not found. Please run `mindsim auth` or pass the key to the constructor.",
138
+ "API Key not found. Please run `mindsim auth`, set MINDSIM_SERVICE_JSON/MINDSIM_SERVICE_JSON_BASE64, or pass credentials to the constructor.",
37
139
  );
38
140
  }
39
141
 
@@ -45,6 +147,10 @@ export class MindSim {
45
147
  "x-reasoner-source": "sdk",
46
148
  "x-reasoner-sdk": "mindsim/mindsim-sdk-typescript",
47
149
  "x-reasoner-sdk-version": sdkVersion,
150
+ // Include project ID from service JSON for better request tracing
151
+ ...(this.serviceJson && {
152
+ "x-mindsim-project-id": this.serviceJson.project_id,
153
+ }),
48
154
  },
49
155
  });
50
156
 
@@ -56,7 +162,73 @@ export class MindSim {
56
162
  this.simulations = new SimulationsResource(this.client);
57
163
  this.tags = new TagsResource(this.client);
58
164
  }
165
+
166
+ /**
167
+ * Get the loaded service JSON (if any).
168
+ * Useful for debugging or accessing project metadata.
169
+ *
170
+ * @returns The service JSON object or null if not using service JSON auth
171
+ */
172
+ public getServiceJson(): MindsimServiceJson | null {
173
+ return this.serviceJson;
174
+ }
175
+
176
+ // ==========================================================================
177
+ // Gap 14 Fix: Auth Method Detection Helpers
178
+ // ==========================================================================
179
+
180
+ /**
181
+ * Check if the client is using service JSON authentication.
182
+ *
183
+ * @returns true if using service JSON, false otherwise
184
+ */
185
+ public isUsingServiceJsonAuth(): boolean {
186
+ return this.serviceJson !== null;
187
+ }
188
+
189
+ /**
190
+ * Get the authentication method used by this client.
191
+ *
192
+ * @returns The auth method: 'service_json', 'api_key_env', 'api_key_constructor', or 'config_file'
193
+ */
194
+ public getAuthMethod(): AuthMethod {
195
+ return this.authMethod;
196
+ }
197
+
198
+ /**
199
+ * Get authentication info for debugging.
200
+ * Does NOT expose the actual API key.
201
+ *
202
+ * @returns Object with auth details (method, project info if service JSON)
203
+ */
204
+ public getAuthInfo(): {
205
+ method: AuthMethod;
206
+ isServiceJson: boolean;
207
+ projectId: string | null;
208
+ projectName: string | null;
209
+ environment: string | null;
210
+ } {
211
+ return {
212
+ method: this.authMethod,
213
+ isServiceJson: this.serviceJson !== null,
214
+ projectId: this.serviceJson?.project_id ?? null,
215
+ projectName: this.serviceJson?.project_name ?? null,
216
+ environment: this.serviceJson?.environment ?? null,
217
+ };
218
+ }
59
219
  }
60
220
 
221
+ // Export validation utilities from config
222
+ export { type ServiceJsonValidationResult, validateServiceJson } from "./config";
223
+
224
+ // Export logger interface (Gap 8 Fix)
225
+ export {
226
+ createSilentLogger,
227
+ createVerboseLogger,
228
+ defaultLogger,
229
+ getLogger,
230
+ type MindSimLogger,
231
+ setLogger,
232
+ } from "./logger";
61
233
  // Export types for consumer use
62
234
  export * from "./types";