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/README.md +110 -2
- package/dist/cli.js +96 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +58 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +357 -13
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +60 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +131 -4
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +63 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +98 -0
- package/dist/logger.js.map +1 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -1
- package/dist/version.js.map +1 -1
- package/package.json +2 -1
- package/src/cli.ts +106 -0
- package/src/config.ts +409 -17
- package/src/index.ts +177 -5
- package/src/logger.ts +111 -0
- package/src/types.ts +59 -0
- package/src/version.ts +2 -1
- package/tests/config.test.ts +465 -1
- package/tests/service-json.test.ts +567 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
390
|
+
// Priority 1: Service JSON
|
|
391
|
+
const serviceJson = loadServiceJson();
|
|
392
|
+
if (serviceJson?.api_key) {
|
|
393
|
+
return serviceJson.api_key;
|
|
24
394
|
}
|
|
25
395
|
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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:
|
|
52
|
-
tokenUrl:
|
|
53
|
-
clientId:
|
|
54
|
-
sdkKeysApiUrl:
|
|
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
|
-
|
|
59
|
-
|
|
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?:
|
|
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
|
-
|
|
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
|
|
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";
|