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/logger.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * MindSim SDK Logger Interface
3
+ *
4
+ * Provides a configurable logging interface so users can control
5
+ * how SDK messages are logged (or suppressed).
6
+ *
7
+ * Gap 8 Fix: Replaces hardcoded console.error with configurable interface.
8
+ *
9
+ * Usage:
10
+ * import { setLogger, createSilentLogger } from 'mindsim';
11
+ *
12
+ * // Use silent logger to suppress all SDK logs
13
+ * setLogger(createSilentLogger());
14
+ *
15
+ * // Use custom logger
16
+ * setLogger({
17
+ * debug: (msg) => myLogger.debug(`[MindSim] ${msg}`),
18
+ * info: (msg) => myLogger.info(`[MindSim] ${msg}`),
19
+ * warn: (msg) => myLogger.warn(`[MindSim] ${msg}`),
20
+ * error: (msg) => myLogger.error(`[MindSim] ${msg}`),
21
+ * });
22
+ */
23
+
24
+ /**
25
+ * Logger interface for the MindSim SDK.
26
+ * Implement this interface to customize SDK logging behavior.
27
+ */
28
+ export interface MindSimLogger {
29
+ /** Debug-level messages (not shown by default) */
30
+ debug: (message: string, ...args: unknown[]) => void;
31
+ /** Informational messages */
32
+ info: (message: string, ...args: unknown[]) => void;
33
+ /** Warning messages */
34
+ warn: (message: string, ...args: unknown[]) => void;
35
+ /** Error messages */
36
+ error: (message: string, ...args: unknown[]) => void;
37
+ }
38
+
39
+ /**
40
+ * Default logger that uses console methods.
41
+ * Debug is disabled by default.
42
+ */
43
+ export const defaultLogger: MindSimLogger = {
44
+ debug: () => {
45
+ // Debug disabled by default
46
+ },
47
+ info: (message: string, ...args: unknown[]) => {
48
+ console.info(`[MindSim] ${message}`, ...args);
49
+ },
50
+ warn: (message: string, ...args: unknown[]) => {
51
+ console.warn(`[MindSim] ${message}`, ...args);
52
+ },
53
+ error: (message: string, ...args: unknown[]) => {
54
+ console.error(`[MindSim] ${message}`, ...args);
55
+ },
56
+ };
57
+
58
+ /**
59
+ * Create a silent logger that suppresses all SDK logs.
60
+ * Useful for production environments where you want no SDK output.
61
+ */
62
+ export function createSilentLogger(): MindSimLogger {
63
+ const noop = () => {};
64
+ return {
65
+ debug: noop,
66
+ info: noop,
67
+ warn: noop,
68
+ error: noop,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Create a logger with all levels enabled (including debug).
74
+ * Useful for development and debugging.
75
+ */
76
+ export function createVerboseLogger(): MindSimLogger {
77
+ return {
78
+ debug: (message: string, ...args: unknown[]) => {
79
+ console.debug(`[MindSim:DEBUG] ${message}`, ...args);
80
+ },
81
+ info: (message: string, ...args: unknown[]) => {
82
+ console.info(`[MindSim] ${message}`, ...args);
83
+ },
84
+ warn: (message: string, ...args: unknown[]) => {
85
+ console.warn(`[MindSim] ${message}`, ...args);
86
+ },
87
+ error: (message: string, ...args: unknown[]) => {
88
+ console.error(`[MindSim] ${message}`, ...args);
89
+ },
90
+ };
91
+ }
92
+
93
+ // Global logger instance
94
+ let currentLogger: MindSimLogger = defaultLogger;
95
+
96
+ /**
97
+ * Set the global logger for the SDK.
98
+ *
99
+ * @param logger - The logger implementation to use
100
+ */
101
+ export function setLogger(logger: MindSimLogger): void {
102
+ currentLogger = logger;
103
+ }
104
+
105
+ /**
106
+ * Get the current logger.
107
+ * Used internally by the SDK.
108
+ */
109
+ export function getLogger(): MindSimLogger {
110
+ return currentLogger;
111
+ }
package/src/types.ts CHANGED
@@ -55,6 +55,65 @@ export interface SdkKeyDetailResponse {
55
55
  };
56
56
  }
57
57
 
58
+ /**
59
+ * Valid MindSim scopes for API access.
60
+ * Matches rainmaker's MindsimScope type (lib/types/developer-portal.ts).
61
+ */
62
+ export type MindsimScope =
63
+ | "minds:read" // List/read digital twins
64
+ | "minds:write" // Create/update digital twins
65
+ | "simulate:run" // Run simulations
66
+ | "simulate:read" // Read simulation history
67
+ | "users:read" // Read org users
68
+ | "users:write" // Manage org users
69
+ | "org:admin"; // Full org administration
70
+
71
+ /**
72
+ * Valid use cases for developer apps.
73
+ * Matches rainmaker's DeveloperUseCase type.
74
+ */
75
+ export type DeveloperUseCase = "internal-workflow" | "customer-facing" | "agentic-ai";
76
+
77
+ /**
78
+ * MindSim Service JSON credential format.
79
+ * Google Cloud-style service account JSON for production deployments.
80
+ */
81
+ export interface MindsimServiceJson {
82
+ /** Must be "mindsim_service_account" to identify this as a service JSON */
83
+ type: "mindsim_service_account";
84
+ /** Service JSON schema version */
85
+ version: string;
86
+ /** App slug (human-readable identifier) */
87
+ project_id: string;
88
+ /** App display name */
89
+ project_name: string;
90
+ /** Environment (always "production" for service JSON) */
91
+ environment: "production";
92
+ /** Service account email (service@{slug}.mindsim.io) */
93
+ client_email: string;
94
+ /** API key UUID */
95
+ client_id: string;
96
+ /** The actual API key secret */
97
+ api_key: string;
98
+ /** API endpoint base URL */
99
+ api_base_url: string;
100
+ /** Granted API scopes */
101
+ scopes: MindsimScope[];
102
+ /** Application metadata */
103
+ app_metadata: {
104
+ app_id: string;
105
+ workspace_id: string;
106
+ mindsim_org_id: string;
107
+ use_case: DeveloperUseCase;
108
+ };
109
+ /** ISO 8601 timestamp when credentials were created */
110
+ created_at: string;
111
+ /** Optional: ISO 8601 timestamp when credentials expire */
112
+ expires_at?: string;
113
+ /** Optional: ISO 8601 timestamp when credentials were issued (same as created_at for new keys) */
114
+ issued_at?: string;
115
+ }
116
+
58
117
  export interface Tag {
59
118
  id: string;
60
119
  name: string;
package/src/version.ts CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import axios from "axios";
6
6
  import semver from "semver";
7
+ import { getLogger } from "./logger";
7
8
 
8
9
  const execAsync = promisify(exec);
9
10
 
@@ -65,7 +66,7 @@ export const getPackageVersion = (): string => {
65
66
  const pkg = JSON.parse(content);
66
67
  return pkg.version;
67
68
  } catch (error) {
68
- console.warn("MindSim SDK: Unable to determine current package version.", error);
69
+ getLogger().warn("Unable to determine current package version.", error);
69
70
  return "0.0.0";
70
71
  }
71
72
  };
@@ -2,7 +2,15 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it, type Mocked, vi } from "vitest";
5
- import { getApiBaseUrl, getDeviceAuthConfig, loadApiKey, saveApiKey } from "../src/config";
5
+ import {
6
+ getApiBaseUrl,
7
+ getDeviceAuthConfig,
8
+ loadApiKey,
9
+ loadServiceJson,
10
+ loadServiceJsonFromBase64,
11
+ saveApiKey,
12
+ validateServiceJson,
13
+ } from "../src/config";
6
14
 
7
15
  // Mock Node built-ins
8
16
  vi.mock("node:fs");
@@ -130,5 +138,461 @@ describe("Config Module", () => {
130
138
  vi.stubEnv("MIND_SIM_API_BASE_URL", "https://staging.api.mindsim.com");
131
139
  expect(getApiBaseUrl()).toBe("https://staging.api.mindsim.com");
132
140
  });
141
+
142
+ it("should prioritize MINDSIM_API_BASE_URL over deprecated MIND_SIM_API_BASE_URL", () => {
143
+ vi.stubEnv("MINDSIM_API_BASE_URL", "https://new-api.reasoner.com");
144
+ vi.stubEnv("MIND_SIM_API_BASE_URL", "https://old-api.reasoner.com");
145
+ expect(getApiBaseUrl()).toBe("https://new-api.reasoner.com");
146
+ });
147
+ });
148
+
149
+ describe("validateServiceJson", () => {
150
+ // Valid service JSON fixture
151
+ const createValidServiceJson = (overrides?: Record<string, unknown>) => ({
152
+ type: "mindsim_service_account",
153
+ version: "1",
154
+ project_id: "my-app",
155
+ project_name: "My App",
156
+ environment: "production",
157
+ client_email: "service@my-app.mindsim.io",
158
+ client_id: "550e8400-e29b-41d4-a716-446655440000",
159
+ api_key: "ms_prod_abc123xyz789",
160
+ api_base_url: "https://api.reasoner.com/api/mindsim",
161
+ scopes: ["minds:read", "simulate:run"],
162
+ app_metadata: {
163
+ app_id: "550e8400-e29b-41d4-a716-446655440001",
164
+ workspace_id: "550e8400-e29b-41d4-a716-446655440002",
165
+ mindsim_org_id: "550e8400-e29b-41d4-a716-446655440003",
166
+ use_case: "internal-workflow",
167
+ },
168
+ created_at: "2025-12-21T00:00:00.000Z",
169
+ ...overrides,
170
+ });
171
+
172
+ describe("basic validation", () => {
173
+ it("should accept valid service JSON", () => {
174
+ const result = validateServiceJson(createValidServiceJson());
175
+ expect(result.valid).toBe(true);
176
+ expect(result.errors).toHaveLength(0);
177
+ expect(result.serviceJson).toBeDefined();
178
+ });
179
+
180
+ it("should reject non-object input", () => {
181
+ const result = validateServiceJson(null);
182
+ expect(result.valid).toBe(false);
183
+ expect(result.errors).toContain("Service JSON must be a valid object");
184
+ });
185
+
186
+ it("should reject undefined input", () => {
187
+ const result = validateServiceJson(undefined);
188
+ expect(result.valid).toBe(false);
189
+ });
190
+
191
+ it("should reject string input", () => {
192
+ const result = validateServiceJson("not an object");
193
+ expect(result.valid).toBe(false);
194
+ });
195
+ });
196
+
197
+ describe("required fields validation", () => {
198
+ it("should require type field", () => {
199
+ const result = validateServiceJson(createValidServiceJson({ type: undefined }));
200
+ expect(result.valid).toBe(false);
201
+ expect(result.errors.some((e) => e.includes("type"))).toBe(true);
202
+ });
203
+
204
+ it("should require version field", () => {
205
+ const result = validateServiceJson(createValidServiceJson({ version: undefined }));
206
+ expect(result.valid).toBe(false);
207
+ expect(result.errors.some((e) => e.includes("version"))).toBe(true);
208
+ });
209
+
210
+ it("should require project_id field", () => {
211
+ const result = validateServiceJson(createValidServiceJson({ project_id: undefined }));
212
+ expect(result.valid).toBe(false);
213
+ expect(result.errors.some((e) => e.includes("project_id"))).toBe(true);
214
+ });
215
+
216
+ it("should require api_key field", () => {
217
+ const result = validateServiceJson(createValidServiceJson({ api_key: undefined }));
218
+ expect(result.valid).toBe(false);
219
+ expect(result.errors.some((e) => e.includes("api_key"))).toBe(true);
220
+ });
221
+ });
222
+
223
+ describe("type field validation", () => {
224
+ it('should require type to be "mindsim_service_account"', () => {
225
+ const result = validateServiceJson(createValidServiceJson({ type: "other_type" }));
226
+ expect(result.valid).toBe(false);
227
+ expect(result.errors.some((e) => e.includes("mindsim_service_account"))).toBe(true);
228
+ });
229
+ });
230
+
231
+ describe("environment field validation", () => {
232
+ it('should require environment to be "production"', () => {
233
+ const result = validateServiceJson(createValidServiceJson({ environment: "development" }));
234
+ expect(result.valid).toBe(false);
235
+ expect(result.errors.some((e) => e.includes("production"))).toBe(true);
236
+ });
237
+ });
238
+
239
+ describe("email validation (Gap 8)", () => {
240
+ it("should accept valid email addresses", () => {
241
+ const result = validateServiceJson(
242
+ createValidServiceJson({ client_email: "service@my-app.mindsim.io" }),
243
+ );
244
+ expect(result.valid).toBe(true);
245
+ });
246
+
247
+ it("should reject email without @", () => {
248
+ const result = validateServiceJson(
249
+ createValidServiceJson({ client_email: "invalid-email" }),
250
+ );
251
+ expect(result.valid).toBe(false);
252
+ expect(result.errors.some((e) => e.includes("valid email"))).toBe(true);
253
+ });
254
+
255
+ it("should reject email without domain", () => {
256
+ const result = validateServiceJson(createValidServiceJson({ client_email: "user@" }));
257
+ expect(result.valid).toBe(false);
258
+ expect(result.errors.some((e) => e.includes("valid email"))).toBe(true);
259
+ });
260
+
261
+ it("should reject email with spaces", () => {
262
+ const result = validateServiceJson(
263
+ createValidServiceJson({ client_email: "user @domain.com" }),
264
+ );
265
+ expect(result.valid).toBe(false);
266
+ });
267
+ });
268
+
269
+ describe("UUID validation", () => {
270
+ it("should validate client_id as UUID", () => {
271
+ const result = validateServiceJson(
272
+ createValidServiceJson({ client_id: "not-a-valid-uuid" }),
273
+ );
274
+ expect(result.valid).toBe(false);
275
+ expect(result.errors.some((e) => e.includes("client_id") && e.includes("UUID"))).toBe(true);
276
+ });
277
+
278
+ it("should validate app_metadata.app_id as UUID", () => {
279
+ const result = validateServiceJson(
280
+ createValidServiceJson({
281
+ app_metadata: {
282
+ app_id: "invalid",
283
+ workspace_id: "550e8400-e29b-41d4-a716-446655440000",
284
+ mindsim_org_id: "550e8400-e29b-41d4-a716-446655440000",
285
+ use_case: "internal-workflow",
286
+ },
287
+ }),
288
+ );
289
+ expect(result.valid).toBe(false);
290
+ expect(result.errors.some((e) => e.includes("app_id") && e.includes("UUID"))).toBe(true);
291
+ });
292
+
293
+ it("should validate app_metadata.workspace_id as UUID", () => {
294
+ const result = validateServiceJson(
295
+ createValidServiceJson({
296
+ app_metadata: {
297
+ app_id: "550e8400-e29b-41d4-a716-446655440000",
298
+ workspace_id: "not-uuid",
299
+ mindsim_org_id: "550e8400-e29b-41d4-a716-446655440000",
300
+ use_case: "internal-workflow",
301
+ },
302
+ }),
303
+ );
304
+ expect(result.valid).toBe(false);
305
+ expect(result.errors.some((e) => e.includes("workspace_id") && e.includes("UUID"))).toBe(
306
+ true,
307
+ );
308
+ });
309
+
310
+ it("should validate app_metadata.mindsim_org_id as UUID", () => {
311
+ const result = validateServiceJson(
312
+ createValidServiceJson({
313
+ app_metadata: {
314
+ app_id: "550e8400-e29b-41d4-a716-446655440000",
315
+ workspace_id: "550e8400-e29b-41d4-a716-446655440000",
316
+ mindsim_org_id: "bad-uuid",
317
+ use_case: "internal-workflow",
318
+ },
319
+ }),
320
+ );
321
+ expect(result.valid).toBe(false);
322
+ expect(result.errors.some((e) => e.includes("mindsim_org_id") && e.includes("UUID"))).toBe(
323
+ true,
324
+ );
325
+ });
326
+ });
327
+
328
+ describe("URL validation", () => {
329
+ it("should validate api_base_url as HTTPS URL", () => {
330
+ const result = validateServiceJson(
331
+ createValidServiceJson({ api_base_url: "http://insecure.com" }),
332
+ );
333
+ expect(result.valid).toBe(false);
334
+ expect(result.errors.some((e) => e.includes("api_base_url") && e.includes("HTTPS"))).toBe(
335
+ true,
336
+ );
337
+ });
338
+
339
+ it("should accept valid HTTPS URL", () => {
340
+ const result = validateServiceJson(
341
+ createValidServiceJson({ api_base_url: "https://api.example.com/v1" }),
342
+ );
343
+ expect(result.valid).toBe(true);
344
+ });
345
+
346
+ it("should reject invalid URL format", () => {
347
+ const result = validateServiceJson(createValidServiceJson({ api_base_url: "not-a-url" }));
348
+ expect(result.valid).toBe(false);
349
+ });
350
+ });
351
+
352
+ describe("timestamp validation", () => {
353
+ it("should validate created_at as ISO 8601", () => {
354
+ const result = validateServiceJson(
355
+ createValidServiceJson({ created_at: "not-a-timestamp" }),
356
+ );
357
+ expect(result.valid).toBe(false);
358
+ expect(result.errors.some((e) => e.includes("created_at") && e.includes("ISO 8601"))).toBe(
359
+ true,
360
+ );
361
+ });
362
+
363
+ it("should accept valid ISO 8601 timestamp", () => {
364
+ const result = validateServiceJson(
365
+ createValidServiceJson({ created_at: "2025-12-21T12:30:45.123Z" }),
366
+ );
367
+ expect(result.valid).toBe(true);
368
+ });
369
+
370
+ it("should validate expires_at if present", () => {
371
+ const result = validateServiceJson(createValidServiceJson({ expires_at: "bad-date" }));
372
+ expect(result.valid).toBe(false);
373
+ expect(result.errors.some((e) => e.includes("expires_at"))).toBe(true);
374
+ });
375
+ });
376
+
377
+ describe("scopes validation", () => {
378
+ it("should require scopes to be an array", () => {
379
+ const result = validateServiceJson(createValidServiceJson({ scopes: "not-array" }));
380
+ expect(result.valid).toBe(false);
381
+ expect(result.errors.some((e) => e.includes("scopes") && e.includes("array"))).toBe(true);
382
+ });
383
+
384
+ it("should reject empty scopes array", () => {
385
+ const result = validateServiceJson(createValidServiceJson({ scopes: [] }));
386
+ expect(result.valid).toBe(false);
387
+ expect(result.errors.some((e) => e.includes("scopes") && e.includes("empty"))).toBe(true);
388
+ });
389
+
390
+ it("should reject invalid scope values", () => {
391
+ const result = validateServiceJson(
392
+ createValidServiceJson({ scopes: ["minds:read", "invalid:scope"] }),
393
+ );
394
+ expect(result.valid).toBe(false);
395
+ expect(result.errors.some((e) => e.includes("invalid:scope"))).toBe(true);
396
+ });
397
+
398
+ it("should accept all valid scopes", () => {
399
+ const validScopes = [
400
+ "minds:read",
401
+ "minds:write",
402
+ "simulate:run",
403
+ "simulate:read",
404
+ "users:read",
405
+ "users:write",
406
+ "org:admin",
407
+ ];
408
+ const result = validateServiceJson(createValidServiceJson({ scopes: validScopes }));
409
+ expect(result.valid).toBe(true);
410
+ });
411
+ });
412
+
413
+ describe("app_metadata validation", () => {
414
+ it("should require app_metadata object", () => {
415
+ const result = validateServiceJson(createValidServiceJson({ app_metadata: undefined }));
416
+ expect(result.valid).toBe(false);
417
+ expect(result.errors.some((e) => e.includes("app_metadata"))).toBe(true);
418
+ });
419
+
420
+ it("should require use_case in app_metadata", () => {
421
+ const result = validateServiceJson(
422
+ createValidServiceJson({
423
+ app_metadata: {
424
+ app_id: "550e8400-e29b-41d4-a716-446655440000",
425
+ workspace_id: "550e8400-e29b-41d4-a716-446655440000",
426
+ mindsim_org_id: "550e8400-e29b-41d4-a716-446655440000",
427
+ use_case: "invalid-use-case",
428
+ },
429
+ }),
430
+ );
431
+ expect(result.valid).toBe(false);
432
+ expect(result.errors.some((e) => e.includes("use_case"))).toBe(true);
433
+ });
434
+
435
+ it("should accept valid use_case values", () => {
436
+ for (const useCase of ["internal-workflow", "customer-facing", "agentic-ai"]) {
437
+ const result = validateServiceJson(
438
+ createValidServiceJson({
439
+ app_metadata: {
440
+ app_id: "550e8400-e29b-41d4-a716-446655440000",
441
+ workspace_id: "550e8400-e29b-41d4-a716-446655440000",
442
+ mindsim_org_id: "550e8400-e29b-41d4-a716-446655440000",
443
+ use_case: useCase,
444
+ },
445
+ }),
446
+ );
447
+ expect(result.valid).toBe(true);
448
+ }
449
+ });
450
+ });
451
+ });
452
+
453
+ describe("loadServiceJsonFromBase64", () => {
454
+ const validServiceJson = {
455
+ type: "mindsim_service_account",
456
+ version: "1",
457
+ project_id: "test-app",
458
+ project_name: "Test App",
459
+ environment: "production",
460
+ client_email: "service@test-app.mindsim.io",
461
+ client_id: "550e8400-e29b-41d4-a716-446655440000",
462
+ api_key: "ms_prod_abc123",
463
+ api_base_url: "https://api.reasoner.com/api/mindsim",
464
+ scopes: ["minds:read"],
465
+ app_metadata: {
466
+ app_id: "550e8400-e29b-41d4-a716-446655440000",
467
+ workspace_id: "550e8400-e29b-41d4-a716-446655440000",
468
+ mindsim_org_id: "550e8400-e29b-41d4-a716-446655440000",
469
+ use_case: "internal-workflow",
470
+ },
471
+ created_at: "2025-12-21T00:00:00.000Z",
472
+ };
473
+
474
+ it("should decode valid base64 service JSON", () => {
475
+ const base64 = Buffer.from(JSON.stringify(validServiceJson)).toString("base64");
476
+ const result = loadServiceJsonFromBase64(base64);
477
+ expect(result).not.toBeNull();
478
+ expect(result?.project_id).toBe("test-app");
479
+ });
480
+
481
+ it("should return null for empty base64 string", () => {
482
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
483
+ const result = loadServiceJsonFromBase64("");
484
+ expect(result).toBeNull();
485
+ consoleSpy.mockRestore();
486
+ });
487
+
488
+ it("should return null for invalid base64", () => {
489
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
490
+ const result = loadServiceJsonFromBase64("not-valid-base64!!!");
491
+ expect(result).toBeNull();
492
+ consoleSpy.mockRestore();
493
+ });
494
+
495
+ it("should return null for base64 with invalid JSON", () => {
496
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
497
+ const base64 = Buffer.from("not valid json").toString("base64");
498
+ const result = loadServiceJsonFromBase64(base64);
499
+ expect(result).toBeNull();
500
+ consoleSpy.mockRestore();
501
+ });
502
+ });
503
+
504
+ describe("loadServiceJson", () => {
505
+ it("should load from MINDSIM_SERVICE_JSON file path", () => {
506
+ const validServiceJson = {
507
+ type: "mindsim_service_account",
508
+ version: "1",
509
+ project_id: "test-app",
510
+ project_name: "Test App",
511
+ environment: "production",
512
+ client_email: "service@test-app.mindsim.io",
513
+ client_id: "550e8400-e29b-41d4-a716-446655440000",
514
+ api_key: "ms_prod_abc123",
515
+ api_base_url: "https://api.reasoner.com/api/mindsim",
516
+ scopes: ["minds:read"],
517
+ app_metadata: {
518
+ app_id: "550e8400-e29b-41d4-a716-446655440000",
519
+ workspace_id: "550e8400-e29b-41d4-a716-446655440000",
520
+ mindsim_org_id: "550e8400-e29b-41d4-a716-446655440000",
521
+ use_case: "internal-workflow",
522
+ },
523
+ created_at: "2025-12-21T00:00:00.000Z",
524
+ };
525
+
526
+ vi.stubEnv("MINDSIM_SERVICE_JSON", "/path/to/service.json");
527
+ mockedFs.existsSync.mockReturnValue(true);
528
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(validServiceJson));
529
+
530
+ const result = loadServiceJson();
531
+ expect(result).not.toBeNull();
532
+ expect(result?.project_id).toBe("test-app");
533
+ });
534
+
535
+ it("should load from MINDSIM_SERVICE_JSON_BASE64", () => {
536
+ const validServiceJson = {
537
+ type: "mindsim_service_account",
538
+ version: "1",
539
+ project_id: "base64-app",
540
+ project_name: "Base64 App",
541
+ environment: "production",
542
+ client_email: "service@base64-app.mindsim.io",
543
+ client_id: "550e8400-e29b-41d4-a716-446655440000",
544
+ api_key: "ms_prod_abc123",
545
+ api_base_url: "https://api.reasoner.com/api/mindsim",
546
+ scopes: ["minds:read"],
547
+ app_metadata: {
548
+ app_id: "550e8400-e29b-41d4-a716-446655440000",
549
+ workspace_id: "550e8400-e29b-41d4-a716-446655440000",
550
+ mindsim_org_id: "550e8400-e29b-41d4-a716-446655440000",
551
+ use_case: "internal-workflow",
552
+ },
553
+ created_at: "2025-12-21T00:00:00.000Z",
554
+ };
555
+
556
+ const base64 = Buffer.from(JSON.stringify(validServiceJson)).toString("base64");
557
+ vi.stubEnv("MINDSIM_SERVICE_JSON_BASE64", base64);
558
+ mockedFs.existsSync.mockReturnValue(false); // No file
559
+
560
+ const result = loadServiceJson();
561
+ expect(result).not.toBeNull();
562
+ expect(result?.project_id).toBe("base64-app");
563
+ });
564
+
565
+ it("should prioritize file path over base64", () => {
566
+ const fileServiceJson = {
567
+ type: "mindsim_service_account",
568
+ version: "1",
569
+ project_id: "file-app",
570
+ project_name: "File App",
571
+ environment: "production",
572
+ client_email: "service@file-app.mindsim.io",
573
+ client_id: "550e8400-e29b-41d4-a716-446655440000",
574
+ api_key: "ms_prod_abc123",
575
+ api_base_url: "https://api.reasoner.com/api/mindsim",
576
+ scopes: ["minds:read"],
577
+ app_metadata: {
578
+ app_id: "550e8400-e29b-41d4-a716-446655440000",
579
+ workspace_id: "550e8400-e29b-41d4-a716-446655440000",
580
+ mindsim_org_id: "550e8400-e29b-41d4-a716-446655440000",
581
+ use_case: "internal-workflow",
582
+ },
583
+ created_at: "2025-12-21T00:00:00.000Z",
584
+ };
585
+
586
+ const base64ServiceJson = { ...fileServiceJson, project_id: "base64-app" };
587
+ const base64 = Buffer.from(JSON.stringify(base64ServiceJson)).toString("base64");
588
+
589
+ vi.stubEnv("MINDSIM_SERVICE_JSON", "/path/to/service.json");
590
+ vi.stubEnv("MINDSIM_SERVICE_JSON_BASE64", base64);
591
+ mockedFs.existsSync.mockReturnValue(true);
592
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(fileServiceJson));
593
+
594
+ const result = loadServiceJson();
595
+ expect(result?.project_id).toBe("file-app"); // File takes priority
596
+ });
133
597
  });
134
598
  });