@object-ui/core 3.1.5 → 3.3.0

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.
@@ -9,6 +9,7 @@
9
9
  import { describe, it, expect } from 'vitest';
10
10
  import { ExpressionEvaluator, evaluateExpression, evaluateCondition, evaluatePlainCondition } from '../ExpressionEvaluator';
11
11
  import { ExpressionContext } from '../ExpressionContext';
12
+ import { SafeExpressionParser } from '../SafeExpressionParser';
12
13
 
13
14
  describe('ExpressionContext', () => {
14
15
  it('should create context with initial data', () => {
@@ -129,3 +130,429 @@ describe('evaluatePlainCondition', () => {
129
130
  expect(evaluatePlainCondition('status', { status: 'active' })).toBe(false);
130
131
  });
131
132
  });
133
+
134
+ // ─── CSP Safety Tests ──────────────────────────────────────────────────────
135
+ //
136
+ // These tests verify that expression evaluation does NOT rely on eval() or
137
+ // new Function() and therefore works under strict Content Security Policy
138
+ // headers that forbid 'unsafe-eval'.
139
+ //
140
+ // The SafeExpressionParser is the CSP-safe backend used by ExpressionCache.
141
+ // All tests below exercise it directly as well as through ExpressionEvaluator.
142
+
143
+ describe('SafeExpressionParser — CSP-safe evaluation', () => {
144
+ const parser = new SafeExpressionParser();
145
+
146
+ describe('does not use eval() or new Function()', () => {
147
+ it('evaluates comparison operators without dynamic code execution', () => {
148
+ // This is the exact expression from the bug report.
149
+ expect(
150
+ parser.evaluate(
151
+ "stage !== 'closed_won' && stage !== 'closed_lost'",
152
+ { stage: 'open' }
153
+ )
154
+ ).toBe(true);
155
+
156
+ expect(
157
+ parser.evaluate(
158
+ "stage !== 'closed_won' && stage !== 'closed_lost'",
159
+ { stage: 'closed_won' }
160
+ )
161
+ ).toBe(false);
162
+
163
+ expect(
164
+ parser.evaluate(
165
+ "stage !== 'closed_won' && stage !== 'closed_lost'",
166
+ { stage: 'closed_lost' }
167
+ )
168
+ ).toBe(false);
169
+ });
170
+
171
+ it('evaluates strict equality operators (===, !==)', () => {
172
+ expect(parser.evaluate("status === 'active'", { status: 'active' })).toBe(true);
173
+ expect(parser.evaluate("status === 'active'", { status: 'inactive' })).toBe(false);
174
+ expect(parser.evaluate("status !== 'active'", { status: 'inactive' })).toBe(true);
175
+ });
176
+
177
+ it('evaluates loose equality operators (==, !=)', () => {
178
+ expect(parser.evaluate('count == 0', { count: 0 })).toBe(true);
179
+ expect(parser.evaluate('count != 0', { count: 1 })).toBe(true);
180
+ });
181
+
182
+ it('evaluates relational operators (>, <, >=, <=)', () => {
183
+ expect(parser.evaluate('score >= 90', { score: 95 })).toBe(true);
184
+ expect(parser.evaluate('score >= 90', { score: 85 })).toBe(false);
185
+ expect(parser.evaluate('score <= 90', { score: 85 })).toBe(true);
186
+ expect(parser.evaluate('score > 50 && score < 100', { score: 75 })).toBe(true);
187
+ });
188
+
189
+ it('evaluates logical operators (&&, ||, !)', () => {
190
+ expect(parser.evaluate('isAdmin && isActive', { isAdmin: true, isActive: true })).toBe(true);
191
+ expect(parser.evaluate('isAdmin && isActive', { isAdmin: true, isActive: false })).toBe(false);
192
+ expect(parser.evaluate('isAdmin || isGuest', { isAdmin: false, isGuest: true })).toBe(true);
193
+ expect(parser.evaluate('!isSuspended', { isSuspended: false })).toBe(true);
194
+ expect(parser.evaluate('!isSuspended', { isSuspended: true })).toBe(false);
195
+ });
196
+
197
+ it('evaluates ternary expressions', () => {
198
+ expect(parser.evaluate("score >= 90 ? 'A' : 'B'", { score: 95 })).toBe('A');
199
+ expect(parser.evaluate("score >= 90 ? 'A' : 'B'", { score: 75 })).toBe('B');
200
+ });
201
+
202
+ it('evaluates nested ternary expressions', () => {
203
+ const expr = "status === 'active' ? 'success' : status === 'pending' ? 'warning' : 'default'";
204
+ expect(parser.evaluate(expr, { status: 'active' })).toBe('success');
205
+ expect(parser.evaluate(expr, { status: 'pending' })).toBe('warning');
206
+ expect(parser.evaluate(expr, { status: 'closed' })).toBe('default');
207
+ });
208
+
209
+ it('evaluates arithmetic operators', () => {
210
+ expect(parser.evaluate('price * quantity', { price: 10, quantity: 5 })).toBe(50);
211
+ expect(parser.evaluate('total - discount', { total: 100, discount: 15 })).toBe(85);
212
+ expect(parser.evaluate('total / count', { total: 60, count: 3 })).toBe(20);
213
+ expect(parser.evaluate('value % 3', { value: 10 })).toBe(1);
214
+ });
215
+
216
+ it('evaluates parenthesized expressions', () => {
217
+ expect(parser.evaluate('(a + b) * c', { a: 2, b: 3, c: 4 })).toBe(20);
218
+ });
219
+
220
+ it('evaluates unary operators', () => {
221
+ expect(parser.evaluate('-amount', { amount: 5 })).toBe(-5);
222
+ expect(parser.evaluate('+amount', { amount: '42' })).toBe(42);
223
+ expect(parser.evaluate('!flag', { flag: false })).toBe(true);
224
+ });
225
+
226
+ it('evaluates typeof operator', () => {
227
+ expect(parser.evaluate('typeof name', { name: 'Alice' })).toBe('string');
228
+ expect(parser.evaluate('typeof count', { count: 42 })).toBe('number');
229
+ });
230
+ });
231
+
232
+ describe('property and method access', () => {
233
+ it('evaluates dot notation property access', () => {
234
+ expect(parser.evaluate('user.name', { user: { name: 'Alice' } })).toBe('Alice');
235
+ expect(parser.evaluate('user.address.city', {
236
+ user: { address: { city: 'London' } }
237
+ })).toBe('London');
238
+ });
239
+
240
+ it('evaluates bracket notation property access', () => {
241
+ expect(parser.evaluate("record['status']", { record: { status: 'active' } })).toBe('active');
242
+ expect(parser.evaluate('arr[0]', { arr: ['first', 'second'] })).toBe('first');
243
+ });
244
+
245
+ it('evaluates optional chaining (?.) gracefully', () => {
246
+ expect(parser.evaluate('user?.address?.city', { user: null })).toBeUndefined();
247
+ expect(parser.evaluate('user?.address?.city', { user: { address: { city: 'NYC' } } })).toBe('NYC');
248
+ });
249
+
250
+ it('evaluates string method calls', () => {
251
+ expect(parser.evaluate('name.toUpperCase()', { name: 'hello' })).toBe('HELLO');
252
+ expect(parser.evaluate('name.toLowerCase()', { name: 'WORLD' })).toBe('world');
253
+ expect(parser.evaluate('name.trim()', { name: ' hi ' })).toBe('hi');
254
+ expect(parser.evaluate("name.includes('ell')", { name: 'hello' })).toBe(true);
255
+ });
256
+
257
+ it('evaluates array methods with arrow functions', () => {
258
+ const items = [
259
+ { name: 'apple', price: 1.5, active: true },
260
+ { name: 'banana', price: 0.75, active: false },
261
+ { name: 'cherry', price: 3.0, active: true },
262
+ ];
263
+ expect(
264
+ parser.evaluate('items.filter(i => i.active).length', { items })
265
+ ).toBe(2);
266
+ expect(
267
+ parser.evaluate('items.map(i => i.name).join(", ")', { items })
268
+ ).toBe('apple, banana, cherry');
269
+ expect(
270
+ parser.evaluate('items.filter(i => i.price > 1).length', { items })
271
+ ).toBe(2);
272
+ });
273
+
274
+ it('evaluates chained array method calls', () => {
275
+ const users = [
276
+ { name: 'Alice', isActive: true },
277
+ { name: 'Bob', isActive: false },
278
+ { name: 'Carol', isActive: true },
279
+ ];
280
+ expect(
281
+ parser.evaluate('users.filter(u => u.isActive).map(u => u.name).join(", ")', { users })
282
+ ).toBe('Alice, Carol');
283
+ });
284
+
285
+ it('evaluates array .length property', () => {
286
+ expect(parser.evaluate('items.length === 0', { items: [] })).toBe(true);
287
+ expect(parser.evaluate('items.length', { items: [1, 2, 3] })).toBe(3);
288
+ });
289
+
290
+ it('evaluates number method calls', () => {
291
+ expect(parser.evaluate('price.toFixed(2)', { price: 3.14159 })).toBe('3.14');
292
+ });
293
+
294
+ it('evaluates Math global functions', () => {
295
+ expect(parser.evaluate('Math.round(value)', { value: 3.7 })).toBe(4);
296
+ expect(parser.evaluate('Math.floor(value)', { value: 3.9 })).toBe(3);
297
+ expect(parser.evaluate('Math.abs(value)', { value: -5 })).toBe(5);
298
+ expect(parser.evaluate('Math.max(a, b)', { a: 10, b: 7 })).toBe(10);
299
+ });
300
+ });
301
+
302
+ describe('literals', () => {
303
+ it('evaluates boolean literals', () => {
304
+ expect(parser.evaluate('true', {})).toBe(true);
305
+ expect(parser.evaluate('false', {})).toBe(false);
306
+ });
307
+
308
+ it('evaluates null and undefined literals', () => {
309
+ expect(parser.evaluate('null', {})).toBeNull();
310
+ expect(parser.evaluate('undefined', {})).toBeUndefined();
311
+ });
312
+
313
+ it('evaluates numeric literals', () => {
314
+ expect(parser.evaluate('42', {})).toBe(42);
315
+ expect(parser.evaluate('3.14', {})).toBeCloseTo(3.14);
316
+ expect(parser.evaluate('1e3', {})).toBe(1000);
317
+ });
318
+
319
+ it('evaluates string literals with single and double quotes', () => {
320
+ expect(parser.evaluate("'hello'", {})).toBe('hello');
321
+ expect(parser.evaluate('"world"', {})).toBe('world');
322
+ });
323
+
324
+ it('evaluates string escape sequences', () => {
325
+ expect(parser.evaluate("'line1\\nline2'", {})).toBe('line1\nline2');
326
+ expect(parser.evaluate("'tab\\there'", {})).toBe('tab\there');
327
+ });
328
+
329
+ it('evaluates array literals', () => {
330
+ expect(parser.evaluate('[1, 2, 3]', {})).toEqual([1, 2, 3]);
331
+ expect(parser.evaluate("['a', 'b']", {})).toEqual(['a', 'b']);
332
+ });
333
+
334
+ it('evaluates NaN and Infinity literals', () => {
335
+ expect(parser.evaluate('Infinity', {})).toBe(Infinity);
336
+ expect(Number.isNaN(parser.evaluate('NaN', {}) as number)).toBe(true);
337
+ });
338
+ });
339
+
340
+ describe('nullish coalescing', () => {
341
+ it('evaluates ?? operator', () => {
342
+ expect(parser.evaluate('value ?? "default"', { value: null })).toBe('default');
343
+ expect(parser.evaluate('value ?? "default"', { value: undefined })).toBe('default');
344
+ expect(parser.evaluate('value ?? "default"', { value: 0 })).toBe(0);
345
+ expect(parser.evaluate('value ?? "default"', { value: '' })).toBe('');
346
+ });
347
+ });
348
+
349
+ describe('short-circuit evaluation', () => {
350
+ it('|| does not evaluate RHS when LHS is truthy', () => {
351
+ // 'missingVar' is not in context — would throw ReferenceError without short-circuit.
352
+ expect(parser.evaluate('true || missingVar', {})).toBe(true);
353
+ });
354
+
355
+ it('&& does not evaluate RHS when LHS is falsy', () => {
356
+ expect(parser.evaluate('false && missingVar', {})).toBe(false);
357
+ });
358
+
359
+ it('?? does not evaluate RHS when LHS is not nullish', () => {
360
+ expect(parser.evaluate('"present" ?? missingVar', {})).toBe('present');
361
+ expect(parser.evaluate('0 ?? missingVar', {})).toBe(0);
362
+ expect(parser.evaluate('"" ?? missingVar', {})).toBe('');
363
+ });
364
+
365
+ it('?? DOES evaluate RHS when LHS is null/undefined', () => {
366
+ expect(parser.evaluate('null ?? "fallback"', {})).toBe('fallback');
367
+ expect(parser.evaluate('undefined ?? "fallback"', {})).toBe('fallback');
368
+ });
369
+
370
+ it('ternary true branch: does not evaluate false branch', () => {
371
+ expect(parser.evaluate("true ? 'yes' : missingVar", {})).toBe('yes');
372
+ });
373
+
374
+ it('ternary false branch: does not evaluate true branch', () => {
375
+ expect(parser.evaluate("false ? missingVar : 'no'", {})).toBe('no');
376
+ });
377
+
378
+ it('nested ternary short-circuits correctly', () => {
379
+ const expr = "status === 'a' ? 'alpha' : status === 'b' ? 'beta' : 'other'";
380
+ expect(parser.evaluate(expr, { status: 'a' })).toBe('alpha');
381
+ expect(parser.evaluate(expr, { status: 'b' })).toBe('beta');
382
+ expect(parser.evaluate(expr, { status: 'c' })).toBe('other');
383
+ });
384
+ });
385
+
386
+ describe('sandbox security', () => {
387
+ it('blocks constructor property access via dot notation', () => {
388
+ expect(() =>
389
+ parser.evaluate('name.constructor', { name: 'hello' })
390
+ ).toThrow(TypeError);
391
+ });
392
+
393
+ it('blocks constructor property access via bracket notation', () => {
394
+ expect(() =>
395
+ parser.evaluate("name['constructor']", { name: 'hello' })
396
+ ).toThrow(TypeError);
397
+ });
398
+
399
+ it('blocks __proto__ access', () => {
400
+ expect(() =>
401
+ parser.evaluate('obj.__proto__', { obj: {} })
402
+ ).toThrow(TypeError);
403
+ });
404
+
405
+ it('blocks prototype access', () => {
406
+ expect(() =>
407
+ parser.evaluate('fn.prototype', { fn: () => {} })
408
+ ).toThrow(TypeError);
409
+ });
410
+
411
+ it('blocks constructor method calls', () => {
412
+ expect(() =>
413
+ parser.evaluate("name['constructor']('return 1')()", { name: 'hello' })
414
+ ).toThrow(TypeError);
415
+ });
416
+
417
+ it('does not expose String/Number/Boolean/Array globals (removed to prevent .constructor escape)', () => {
418
+ expect(() => parser.evaluate('String', {})).toThrow(ReferenceError);
419
+ expect(() => parser.evaluate('Number', {})).toThrow(ReferenceError);
420
+ expect(() => parser.evaluate('Boolean', {})).toThrow(ReferenceError);
421
+ expect(() => parser.evaluate('Array', {})).toThrow(ReferenceError);
422
+ });
423
+ });
424
+
425
+ describe('error handling', () => {
426
+ it('throws ReferenceError for undefined identifiers', () => {
427
+ expect(() => parser.evaluate('nonExistentVar', {})).toThrow(ReferenceError);
428
+ });
429
+
430
+ it('returns undefined gracefully for missing property access (no throw)', () => {
431
+ expect(parser.evaluate('user.missingProp', { user: {} })).toBeUndefined();
432
+ expect(parser.evaluate('user.address.city', { user: {} })).toBeUndefined();
433
+ });
434
+
435
+ it('throws SyntaxError for unclosed parentheses', () => {
436
+ // Use a valid inner expression so the error is about the missing ')' not the content.
437
+ expect(() => parser.evaluate('(1 + 2', {})).toThrow(SyntaxError);
438
+ });
439
+
440
+ it('throws SyntaxError for unclosed array literal', () => {
441
+ expect(() => parser.evaluate('[1, 2', {})).toThrow(SyntaxError);
442
+ });
443
+
444
+ it('throws SyntaxError for malformed numeric exponent (e.g. 1e)', () => {
445
+ // '1e' has no exponent digits — the stricter parser rejects it.
446
+ expect(() => parser.evaluate('1e', {})).toThrow(SyntaxError);
447
+ });
448
+ });
449
+ });
450
+
451
+ describe('ExpressionEvaluator — CSP safety integration', () => {
452
+ it('evaluates the bug-report expression without CSP violation', () => {
453
+ // Exact expression from the bug report that was blocked by CSP in production.
454
+ const evaluator = new ExpressionEvaluator({ stage: 'open' });
455
+ expect(
456
+ evaluator.evaluate("${stage !== 'closed_won' && stage !== 'closed_lost'}")
457
+ ).toBe(true);
458
+
459
+ const evaluator2 = new ExpressionEvaluator({ stage: 'closed_won' });
460
+ expect(
461
+ evaluator2.evaluate("${stage !== 'closed_won' && stage !== 'closed_lost'}")
462
+ ).toBe(false);
463
+ });
464
+
465
+ it('supports all comparison operators via the safe parser', () => {
466
+ const e = new ExpressionEvaluator({ a: 10, b: 10, c: 5 });
467
+ expect(e.evaluateExpression('a === b')).toBe(true);
468
+ expect(e.evaluateExpression('a !== c')).toBe(true);
469
+ expect(e.evaluateExpression('a > c')).toBe(true);
470
+ expect(e.evaluateExpression('c < a')).toBe(true);
471
+ expect(e.evaluateExpression('a >= b')).toBe(true);
472
+ expect(e.evaluateExpression('c <= a')).toBe(true);
473
+ });
474
+
475
+ it('supports logical operators via the safe parser', () => {
476
+ const e = new ExpressionEvaluator({ x: true, y: false });
477
+ expect(e.evaluateExpression('x && !y')).toBe(true);
478
+ expect(e.evaluateExpression('x || y')).toBe(true);
479
+ expect(e.evaluateExpression('!x || !y')).toBe(true);
480
+ });
481
+
482
+ it('supports ternary expressions via the safe parser', () => {
483
+ const e = new ExpressionEvaluator({ score: 95 });
484
+ expect(e.evaluateExpression("score >= 90 ? 'A' : 'B'")).toBe('A');
485
+ });
486
+
487
+ it('supports property access via the safe parser', () => {
488
+ const e = new ExpressionEvaluator({ user: { role: 'admin', isActive: true } });
489
+ expect(e.evaluateExpression("user.role === 'admin'")).toBe(true);
490
+ expect(e.evaluateExpression('user.isActive')).toBe(true);
491
+ });
492
+
493
+ it('supports arithmetic via the safe parser', () => {
494
+ const e = new ExpressionEvaluator({ price: 10, qty: 5 });
495
+ expect(e.evaluateExpression('price * qty')).toBe(50);
496
+ });
497
+
498
+ it('supports arrow-function array methods via the safe parser', () => {
499
+ const e = new ExpressionEvaluator({
500
+ items: [{ price: 50 }, { price: 150 }, { price: 200 }],
501
+ });
502
+ expect(e.evaluateExpression('items.filter(item => item.price > 100).length')).toBe(2);
503
+ });
504
+
505
+ it('supports formula functions via the safe parser', () => {
506
+ const e = new ExpressionEvaluator({ values: [10, 20, 30] });
507
+ expect(e.evaluateExpression('SUM(values)')).toBe(60);
508
+ expect(e.evaluateExpression('AVG(values)')).toBe(20);
509
+ });
510
+
511
+ it('supports Math global via the safe parser', () => {
512
+ const e = new ExpressionEvaluator({ value: 3.7 });
513
+ expect(e.evaluateExpression('Math.round(value)')).toBe(4);
514
+ });
515
+
516
+ it('does not use eval() or new Function() during evaluation', () => {
517
+ // Spy on both to ensure they are NEVER called (via construct OR apply).
518
+ const originalEval = globalThis.eval;
519
+ const originalFunction = Function;
520
+ const evalCalls: string[] = [];
521
+ const functionCalls: string[] = [];
522
+
523
+ globalThis.eval = (...args: Parameters<typeof eval>) => {
524
+ evalCalls.push(String(args[0]));
525
+ return originalEval(...args);
526
+ };
527
+
528
+ const FunctionProxy = new Proxy(Function, {
529
+ construct(target, args) {
530
+ functionCalls.push(`new Function(${String(args)})`);
531
+ return Reflect.construct(target, args);
532
+ },
533
+ apply(target, thisArg, args) {
534
+ // Catches indirect calls like: Function('return 1')() or String['constructor']('...')
535
+ functionCalls.push(`Function(${String(args)})`);
536
+ return Reflect.apply(target, thisArg, args);
537
+ },
538
+ });
539
+ (globalThis as any).Function = FunctionProxy;
540
+
541
+ try {
542
+ const e = new ExpressionEvaluator({
543
+ stage: 'open',
544
+ data: { amount: 1500 },
545
+ items: [{ active: true }, { active: false }],
546
+ });
547
+ e.evaluate("${stage !== 'closed_won' && stage !== 'closed_lost'}");
548
+ e.evaluateExpression('data.amount > 1000');
549
+ e.evaluateExpression('items.filter(i => i.active).length');
550
+
551
+ expect(evalCalls).toHaveLength(0);
552
+ expect(functionCalls).toHaveLength(0);
553
+ } finally {
554
+ globalThis.eval = originalEval;
555
+ (globalThis as any).Function = originalFunction;
556
+ }
557
+ });
558
+ });
@@ -10,3 +10,4 @@ export * from './ExpressionContext.js';
10
10
  export * from './ExpressionEvaluator.js';
11
11
  export * from './ExpressionCache.js';
12
12
  export * from './FormulaFunctions.js';
13
+ export * from './SafeExpressionParser.js';
@@ -95,18 +95,10 @@ export function resolveDndConfig(config: DndConfig): ResolvedDndConfig {
95
95
  * @returns Component props object for a draggable element
96
96
  */
97
97
  export function createDragItemProps(item: DragItem): DragItemProps {
98
- const ariaLabel = typeof item.ariaLabel === 'string'
99
- ? item.ariaLabel
100
- : item.ariaLabel?.defaultValue;
101
-
102
- const label = typeof item.label === 'string'
103
- ? item.label
104
- : item.label?.defaultValue;
105
-
106
98
  return {
107
99
  draggable: !(item.disabled ?? false),
108
100
  'aria-roledescription': 'draggable',
109
- 'aria-label': ariaLabel ?? label,
101
+ 'aria-label': item.ariaLabel ?? item.label,
110
102
  'aria-describedby': item.ariaDescribedBy,
111
103
  role: item.role ?? 'listitem',
112
104
  'data-drag-type': item.type,
@@ -127,17 +119,9 @@ export function createDragItemProps(item: DragItem): DragItemProps {
127
119
  * @returns Component props object for a droppable area
128
120
  */
129
121
  export function createDropZoneProps(zone: DropZone): DropZoneProps {
130
- const ariaLabel = typeof zone.ariaLabel === 'string'
131
- ? zone.ariaLabel
132
- : zone.ariaLabel?.defaultValue;
133
-
134
- const label = typeof zone.label === 'string'
135
- ? zone.label
136
- : zone.label?.defaultValue;
137
-
138
122
  return {
139
123
  'aria-dropeffect': zone.dropEffect ?? 'move',
140
- 'aria-label': ariaLabel ?? label,
124
+ 'aria-label': zone.ariaLabel ?? zone.label,
141
125
  'aria-describedby': zone.ariaDescribedBy,
142
126
  role: zone.role ?? 'list',
143
127
  'data-drop-accept': zone.accept.join(','),
@@ -83,15 +83,11 @@ export interface KeyboardEventLike {
83
83
  * @returns Fully resolved keyboard navigation configuration
84
84
  */
85
85
  export function resolveKeyboardConfig(config: KeyboardNavigationConfig): ResolvedKeyboardConfig {
86
- const ariaLabel = typeof config.ariaLabel === 'string'
87
- ? config.ariaLabel
88
- : config.ariaLabel?.defaultValue;
89
-
90
86
  return {
91
87
  shortcuts: config.shortcuts ?? [],
92
88
  focusManagement: resolveFocusManagement(config.focusManagement),
93
89
  rovingTabindex: config.rovingTabindex ?? false,
94
- ariaLabel,
90
+ ariaLabel: config.ariaLabel,
95
91
  ariaDescribedBy: config.ariaDescribedBy,
96
92
  role: config.role,
97
93
  };
@@ -125,15 +125,6 @@ export function resolveNotificationConfig(config: NotificationConfig): ResolvedN
125
125
  // Spec Notification → Toast
126
126
  // ============================================================================
127
127
 
128
- /**
129
- * Extract the display string from a translatable value (string or Translation object).
130
- */
131
- function resolveTranslatableString(value: string | { key: string; defaultValue?: string } | undefined): string | undefined {
132
- if (value === undefined) return undefined;
133
- if (typeof value === 'string') return value;
134
- return value.defaultValue;
135
- }
136
-
137
128
  /**
138
129
  * Convert a spec Notification to a toast-compatible object.
139
130
  *
@@ -142,14 +133,14 @@ function resolveTranslatableString(value: string | { key: string; defaultValue?:
142
133
  */
143
134
  export function specNotificationToToast(notification: SpecNotification): ToastNotification {
144
135
  const actions: ToastAction[] = (notification.actions ?? []).map((a: NotificationAction) => ({
145
- label: typeof a.label === 'string' ? a.label : a.label?.defaultValue ?? '',
136
+ label: a.label,
146
137
  action: a.action,
147
138
  variant: a.variant ?? 'primary',
148
139
  }));
149
140
 
150
141
  return {
151
- title: resolveTranslatableString(notification.title),
152
- description: resolveTranslatableString(notification.message) ?? '',
142
+ title: notification.title,
143
+ description: notification.message ?? '',
153
144
  variant: mapSeverityToVariant(notification.severity ?? 'info'),
154
145
  position: mapPosition(notification.position ?? 'top_right'),
155
146
  duration: notification.duration ?? 5000,
@@ -93,7 +93,8 @@ export function isDebugEnabled(): boolean {
93
93
  if (g === true || g === 'true') return true;
94
94
 
95
95
  // 3. process.env
96
- if (typeof process !== 'undefined' && process.env?.OBJECTUI_DEBUG === 'true') return true;
96
+ const proc = (globalThis as any).process;
97
+ if (proc?.env?.OBJECTUI_DEBUG === 'true') return true;
97
98
 
98
99
  return false;
99
100
  } catch {