@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 +121 -5
- package/package.json +1 -1
- package/src/sandbox/bun-worker.ts +121 -5
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
|
-
//
|
|
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
|
-
|
|
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 + '.' +
|
|
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
|
-
|
|
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
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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 + '.' +
|
|
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
|
-
|
|
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;
|