@papicandela/mcx-core 0.2.6 → 0.2.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/dist/index.js CHANGED
@@ -6488,13 +6488,62 @@ class BunWorkerSandbox {
6488
6488
  return arr.slice(0, n);
6489
6489
  };
6490
6490
 
6491
+ /**
6492
+ * Poll a function until condition is met or max iterations reached.
6493
+ * @param fn - Async function to call. Return { done: true, value } to stop.
6494
+ * @param opts - { interval: ms (default 1000), maxIterations: n (default 10) }
6495
+ * @returns Array of all results, or last result if done:true was returned
6496
+ */
6497
+ globalThis.poll = async (fn, opts = {}) => {
6498
+ const interval = opts.interval ?? 1000;
6499
+ const maxIterations = opts.maxIterations ?? 10;
6500
+ const results = [];
6501
+
6502
+ for (let i = 0; i < maxIterations; i++) {
6503
+ const result = await fn(i);
6504
+ results.push(result);
6505
+
6506
+ // Check for done signal
6507
+ if (result && typeof result === 'object' && result.done) {
6508
+ return result.value !== undefined ? result.value : results;
6509
+ }
6510
+
6511
+ // Wait before next iteration (skip on last)
6512
+ if (i < maxIterations - 1) {
6513
+ await new Promise(r => setTimeout(r, interval));
6514
+ }
6515
+ }
6516
+
6517
+ return results;
6518
+ };
6519
+
6520
+ /**
6521
+ * Wait for a condition to become true.
6522
+ * @param fn - Async function that returns truthy when done
6523
+ * @param opts - { interval: ms (default 500), timeout: ms (default 10000) }
6524
+ * @returns The truthy value returned by fn
6525
+ */
6526
+ globalThis.waitFor = async (fn, opts = {}) => {
6527
+ const interval = opts.interval ?? 500;
6528
+ const timeout = opts.timeout ?? 10000;
6529
+ const start = Date.now();
6530
+
6531
+ while (Date.now() - start < timeout) {
6532
+ const result = await fn();
6533
+ if (result) return result;
6534
+ await new Promise(r => setTimeout(r, interval));
6535
+ }
6536
+
6537
+ throw new Error('waitFor timeout after ' + timeout + 'ms');
6538
+ };
6539
+
6491
6540
  // SECURITY: Reserved keys that must not be overwritten by user-provided variables/globals
6492
6541
  const RESERVED_KEYS = new Set([
6493
6542
  'onmessage', 'postMessage', 'close', 'terminate', 'self',
6494
6543
  'constructor', 'prototype', '__proto__',
6495
6544
  'pendingCalls', 'callId', 'logs', 'console', 'adapters',
6496
6545
  'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',
6497
- 'pick', 'table', 'count', 'sum', 'first', 'safeStr'
6546
+ 'pick', 'table', 'count', 'sum', 'first', 'safeStr', 'poll', 'waitFor'
6498
6547
  ]);
6499
6548
 
6500
6549
  self.onmessage = async (event) => {
@@ -6538,6 +6587,12 @@ class BunWorkerSandbox {
6538
6587
  return matrix[b.length][a.length];
6539
6588
  };
6540
6589
 
6590
+ // Convert camelCase to snake_case
6591
+ const toSnakeCase = (str) => str
6592
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
6593
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
6594
+ .toLowerCase();
6595
+
6541
6596
  // Find similar method names
6542
6597
  const findSimilar = (name, methods, maxDist = 3) => {
6543
6598
  const normalized = name.toLowerCase().replace(/[-_]/g, '');
@@ -6549,6 +6604,22 @@ class BunWorkerSandbox {
6549
6604
  .map(x => x.method);
6550
6605
  };
6551
6606
 
6607
+ // Find best auto-correctable match (distance ≤ 2 = safe to auto-correct)
6608
+ const findAutoCorrect = (name, methods) => {
6609
+ // Try exact snake_case conversion first
6610
+ const snake = toSnakeCase(name);
6611
+ if (methods.includes(snake)) return snake;
6612
+
6613
+ // Try normalized fuzzy match
6614
+ const normalized = name.toLowerCase().replace(/[-_]/g, '');
6615
+ const matches = methods
6616
+ .map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
6617
+ .filter(x => x.dist <= 2) // Only auto-correct if very close
6618
+ .sort((a, b) => a.dist - b.dist);
6619
+
6620
+ return matches.length > 0 ? matches[0].method : null;
6621
+ };
6622
+
6552
6623
  // Create adapter proxies with helpful error messages
6553
6624
  const adaptersObj = {};
6554
6625
  for (const [adapterName, methods] of Object.entries(adapterMethods)) {
@@ -6569,16 +6640,45 @@ class BunWorkerSandbox {
6569
6640
  };
6570
6641
  }
6571
6642
 
6572
- // Use Proxy to intercept undefined method calls
6643
+ // Precompute auto-correct lookup (O(1) instead of O(n) per access)
6644
+ const autoCorrectMap = new Map();
6645
+ for (const m of methods) {
6646
+ // Map snake_case variants: execute_sql -> execute_sql (identity)
6647
+ autoCorrectMap.set(m, m);
6648
+ // Map normalized: executesql -> execute_sql
6649
+ autoCorrectMap.set(m.toLowerCase().replace(/[-_]/g, ''), m);
6650
+ }
6651
+
6652
+ // Use Proxy to intercept undefined method calls with auto-correction
6573
6653
  const adapterProxy = new Proxy(methodsImpl, {
6574
6654
  get(target, prop) {
6575
6655
  if (prop in target) return target[prop];
6576
6656
  if (typeof prop === 'symbol') return undefined;
6577
- const similar = findSimilar(String(prop), methods);
6657
+
6658
+ const propStr = String(prop);
6659
+
6660
+ // Fast path: check precomputed map (O(1))
6661
+ const snake = toSnakeCase(propStr);
6662
+ if (autoCorrectMap.has(snake)) {
6663
+ return target[autoCorrectMap.get(snake)];
6664
+ }
6665
+ const normalized = propStr.toLowerCase().replace(/[-_]/g, '');
6666
+ if (autoCorrectMap.has(normalized)) {
6667
+ return target[autoCorrectMap.get(normalized)];
6668
+ }
6669
+
6670
+ // Slow path: fuzzy match for typos (only if fast path failed)
6671
+ const corrected = findAutoCorrect(propStr, methods);
6672
+ if (corrected && corrected in target) {
6673
+ return target[corrected];
6674
+ }
6675
+
6676
+ // No auto-correct possible, throw with suggestions
6677
+ const similar = findSimilar(propStr, methods);
6578
6678
  const suggestion = similar.length > 0
6579
6679
  ? '. Did you mean: ' + similar.join(', ') + '?'
6580
6680
  : '. Available: ' + methods.slice(0, 5).join(', ') + (methods.length > 5 ? '...' : '');
6581
- throw new Error(adapterName + '.' + String(prop) + ' is not a function' + suggestion);
6681
+ throw new Error(adapterName + '.' + propStr + ' is not a function' + suggestion);
6582
6682
  }
6583
6683
  });
6584
6684
 
@@ -6615,7 +6715,23 @@ class BunWorkerSandbox {
6615
6715
  const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
6616
6716
  const fn = new AsyncFunction(data.code);
6617
6717
  const result = await fn();
6618
- self.postMessage({ type: 'result', success: true, value: result, logs });
6718
+
6719
+ // SECURITY: Hard cap on output size to prevent OOM
6720
+ const HARD_CAP_BYTES = 100 * 1024 * 1024; // 100MB
6721
+ const serialized = safeStr(result);
6722
+ if (serialized.length > HARD_CAP_BYTES) {
6723
+ self.postMessage({
6724
+ type: 'result',
6725
+ success: false,
6726
+ error: {
6727
+ name: 'OutputSizeError',
6728
+ message: 'Output exceeds 100MB limit. Use pagination or filtering.'
6729
+ },
6730
+ logs
6731
+ });
6732
+ } else {
6733
+ self.postMessage({ type: 'result', success: true, value: result, logs });
6734
+ }
6619
6735
  } catch (err) {
6620
6736
  // Truncate stack to 5 lines to prevent context bloat
6621
6737
  const stack = err.stack ? err.stack.split('\\n').slice(0, 5).join('\\n') : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papicandela/mcx-core",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "MCX Core - MCP Code Execution Framework",
5
5
  "author": "papicandela",
6
6
  "license": "MIT",
@@ -285,13 +285,62 @@ export class BunWorkerSandbox implements ISandbox {
285
285
  return arr.slice(0, n);
286
286
  };
287
287
 
288
+ /**
289
+ * Poll a function until condition is met or max iterations reached.
290
+ * @param fn - Async function to call. Return { done: true, value } to stop.
291
+ * @param opts - { interval: ms (default 1000), maxIterations: n (default 10) }
292
+ * @returns Array of all results, or last result if done:true was returned
293
+ */
294
+ globalThis.poll = async (fn, opts = {}) => {
295
+ const interval = opts.interval ?? 1000;
296
+ const maxIterations = opts.maxIterations ?? 10;
297
+ const results = [];
298
+
299
+ for (let i = 0; i < maxIterations; i++) {
300
+ const result = await fn(i);
301
+ results.push(result);
302
+
303
+ // Check for done signal
304
+ if (result && typeof result === 'object' && result.done) {
305
+ return result.value !== undefined ? result.value : results;
306
+ }
307
+
308
+ // Wait before next iteration (skip on last)
309
+ if (i < maxIterations - 1) {
310
+ await new Promise(r => setTimeout(r, interval));
311
+ }
312
+ }
313
+
314
+ return results;
315
+ };
316
+
317
+ /**
318
+ * Wait for a condition to become true.
319
+ * @param fn - Async function that returns truthy when done
320
+ * @param opts - { interval: ms (default 500), timeout: ms (default 10000) }
321
+ * @returns The truthy value returned by fn
322
+ */
323
+ globalThis.waitFor = async (fn, opts = {}) => {
324
+ const interval = opts.interval ?? 500;
325
+ const timeout = opts.timeout ?? 10000;
326
+ const start = Date.now();
327
+
328
+ while (Date.now() - start < timeout) {
329
+ const result = await fn();
330
+ if (result) return result;
331
+ await new Promise(r => setTimeout(r, interval));
332
+ }
333
+
334
+ throw new Error('waitFor timeout after ' + timeout + 'ms');
335
+ };
336
+
288
337
  // SECURITY: Reserved keys that must not be overwritten by user-provided variables/globals
289
338
  const RESERVED_KEYS = new Set([
290
339
  'onmessage', 'postMessage', 'close', 'terminate', 'self',
291
340
  'constructor', 'prototype', '__proto__',
292
341
  'pendingCalls', 'callId', 'logs', 'console', 'adapters',
293
342
  'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',
294
- 'pick', 'table', 'count', 'sum', 'first', 'safeStr'
343
+ 'pick', 'table', 'count', 'sum', 'first', 'safeStr', 'poll', 'waitFor'
295
344
  ]);
296
345
 
297
346
  self.onmessage = async (event) => {
@@ -335,6 +384,12 @@ export class BunWorkerSandbox implements ISandbox {
335
384
  return matrix[b.length][a.length];
336
385
  };
337
386
 
387
+ // Convert camelCase to snake_case
388
+ const toSnakeCase = (str) => str
389
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
390
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
391
+ .toLowerCase();
392
+
338
393
  // Find similar method names
339
394
  const findSimilar = (name, methods, maxDist = 3) => {
340
395
  const normalized = name.toLowerCase().replace(/[-_]/g, '');
@@ -346,6 +401,22 @@ export class BunWorkerSandbox implements ISandbox {
346
401
  .map(x => x.method);
347
402
  };
348
403
 
404
+ // Find best auto-correctable match (distance ≤ 2 = safe to auto-correct)
405
+ const findAutoCorrect = (name, methods) => {
406
+ // Try exact snake_case conversion first
407
+ const snake = toSnakeCase(name);
408
+ if (methods.includes(snake)) return snake;
409
+
410
+ // Try normalized fuzzy match
411
+ const normalized = name.toLowerCase().replace(/[-_]/g, '');
412
+ const matches = methods
413
+ .map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
414
+ .filter(x => x.dist <= 2) // Only auto-correct if very close
415
+ .sort((a, b) => a.dist - b.dist);
416
+
417
+ return matches.length > 0 ? matches[0].method : null;
418
+ };
419
+
349
420
  // Create adapter proxies with helpful error messages
350
421
  const adaptersObj = {};
351
422
  for (const [adapterName, methods] of Object.entries(adapterMethods)) {
@@ -366,16 +437,45 @@ export class BunWorkerSandbox implements ISandbox {
366
437
  };
367
438
  }
368
439
 
369
- // Use Proxy to intercept undefined method calls
440
+ // Precompute auto-correct lookup (O(1) instead of O(n) per access)
441
+ const autoCorrectMap = new Map();
442
+ for (const m of methods) {
443
+ // Map snake_case variants: execute_sql -> execute_sql (identity)
444
+ autoCorrectMap.set(m, m);
445
+ // Map normalized: executesql -> execute_sql
446
+ autoCorrectMap.set(m.toLowerCase().replace(/[-_]/g, ''), m);
447
+ }
448
+
449
+ // Use Proxy to intercept undefined method calls with auto-correction
370
450
  const adapterProxy = new Proxy(methodsImpl, {
371
451
  get(target, prop) {
372
452
  if (prop in target) return target[prop];
373
453
  if (typeof prop === 'symbol') return undefined;
374
- const similar = findSimilar(String(prop), methods);
454
+
455
+ const propStr = String(prop);
456
+
457
+ // Fast path: check precomputed map (O(1))
458
+ const snake = toSnakeCase(propStr);
459
+ if (autoCorrectMap.has(snake)) {
460
+ return target[autoCorrectMap.get(snake)];
461
+ }
462
+ const normalized = propStr.toLowerCase().replace(/[-_]/g, '');
463
+ if (autoCorrectMap.has(normalized)) {
464
+ return target[autoCorrectMap.get(normalized)];
465
+ }
466
+
467
+ // Slow path: fuzzy match for typos (only if fast path failed)
468
+ const corrected = findAutoCorrect(propStr, methods);
469
+ if (corrected && corrected in target) {
470
+ return target[corrected];
471
+ }
472
+
473
+ // No auto-correct possible, throw with suggestions
474
+ const similar = findSimilar(propStr, methods);
375
475
  const suggestion = similar.length > 0
376
476
  ? '. Did you mean: ' + similar.join(', ') + '?'
377
477
  : '. Available: ' + methods.slice(0, 5).join(', ') + (methods.length > 5 ? '...' : '');
378
- throw new Error(adapterName + '.' + String(prop) + ' is not a function' + suggestion);
478
+ throw new Error(adapterName + '.' + propStr + ' is not a function' + suggestion);
379
479
  }
380
480
  });
381
481
 
@@ -412,7 +512,23 @@ export class BunWorkerSandbox implements ISandbox {
412
512
  const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
413
513
  const fn = new AsyncFunction(data.code);
414
514
  const result = await fn();
415
- self.postMessage({ type: 'result', success: true, value: result, logs });
515
+
516
+ // SECURITY: Hard cap on output size to prevent OOM
517
+ const HARD_CAP_BYTES = 100 * 1024 * 1024; // 100MB
518
+ const serialized = safeStr(result);
519
+ if (serialized.length > HARD_CAP_BYTES) {
520
+ self.postMessage({
521
+ type: 'result',
522
+ success: false,
523
+ error: {
524
+ name: 'OutputSizeError',
525
+ message: 'Output exceeds 100MB limit. Use pagination or filtering.'
526
+ },
527
+ logs
528
+ });
529
+ } else {
530
+ self.postMessage({ type: 'result', success: true, value: result, logs });
531
+ }
416
532
  } catch (err) {
417
533
  // Truncate stack to 5 lines to prevent context bloat
418
534
  const stack = err.stack ? err.stack.split('\\n').slice(0, 5).join('\\n') : undefined;