@papicandela/mcx-core 0.2.6 → 0.2.8
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 +224 -5
- package/package.json +1 -1
- package/src/sandbox/bun-worker.ts +224 -5
package/dist/index.js
CHANGED
|
@@ -6488,13 +6488,165 @@ 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
|
+
|
|
6540
|
+
/**
|
|
6541
|
+
* Get lines around a specific line number (1-indexed).
|
|
6542
|
+
* @param file - File object with .lines array
|
|
6543
|
+
* @param line - Line number (1-indexed)
|
|
6544
|
+
* @param ctx - Context lines before/after (default 10)
|
|
6545
|
+
*/
|
|
6546
|
+
globalThis.around = (file, line, ctx = 10) => {
|
|
6547
|
+
const lines = file?.lines;
|
|
6548
|
+
if (!Array.isArray(lines)) return [];
|
|
6549
|
+
if (line < 1 || line > lines.length) return [];
|
|
6550
|
+
const idx = line - 1;
|
|
6551
|
+
return lines.slice(Math.max(0, idx - ctx), Math.min(lines.length, idx + ctx + 1));
|
|
6552
|
+
};
|
|
6553
|
+
|
|
6554
|
+
/**
|
|
6555
|
+
* Extract code block containing a line (detects by indentation).
|
|
6556
|
+
* @param file - File object with .lines array
|
|
6557
|
+
* @param line - Line number (1-indexed)
|
|
6558
|
+
*/
|
|
6559
|
+
globalThis.block = (file, line) => {
|
|
6560
|
+
const lines = file?.lines;
|
|
6561
|
+
if (!Array.isArray(lines)) return [];
|
|
6562
|
+
const idx = line - 1;
|
|
6563
|
+
if (idx < 0 || idx >= lines.length) return [];
|
|
6564
|
+
|
|
6565
|
+
const targetIndent = lines[idx].search(/\\S/);
|
|
6566
|
+
if (targetIndent < 0) return [lines[idx]];
|
|
6567
|
+
|
|
6568
|
+
// Find block start
|
|
6569
|
+
let start = idx;
|
|
6570
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
6571
|
+
const lineIndent = lines[i].search(/\\S/);
|
|
6572
|
+
if (lineIndent >= 0 && lineIndent < targetIndent) { start = i; break; }
|
|
6573
|
+
if (lineIndent === 0 && lines[i].trim()) { start = i; break; }
|
|
6574
|
+
}
|
|
6575
|
+
|
|
6576
|
+
// Find block end
|
|
6577
|
+
const startIndent = lines[start].search(/\\S/);
|
|
6578
|
+
let end = idx;
|
|
6579
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
6580
|
+
const lineIndent = lines[i].search(/\\S/);
|
|
6581
|
+
if (lineIndent >= 0 && lineIndent <= startIndent && lines[i].trim()) { end = i - 1; break; }
|
|
6582
|
+
end = i;
|
|
6583
|
+
}
|
|
6584
|
+
|
|
6585
|
+
return lines.slice(start, end + 1);
|
|
6586
|
+
};
|
|
6587
|
+
|
|
6588
|
+
/**
|
|
6589
|
+
* Grep with context lines.
|
|
6590
|
+
* @param file - File object with .lines array
|
|
6591
|
+
* @param pattern - String or regex pattern
|
|
6592
|
+
* @param ctx - Context lines (default 3)
|
|
6593
|
+
*/
|
|
6594
|
+
globalThis.grep = (file, pattern, ctx = 3) => {
|
|
6595
|
+
const lines = file?.lines;
|
|
6596
|
+
if (!Array.isArray(lines)) return [];
|
|
6597
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern, 'i') : pattern;
|
|
6598
|
+
const results = [];
|
|
6599
|
+
|
|
6600
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6601
|
+
if (regex.test(lines[i])) {
|
|
6602
|
+
results.push({
|
|
6603
|
+
line: i + 1,
|
|
6604
|
+
match: lines[i],
|
|
6605
|
+
context: lines.slice(Math.max(0, i - ctx), i + ctx + 1)
|
|
6606
|
+
});
|
|
6607
|
+
}
|
|
6608
|
+
}
|
|
6609
|
+
return results;
|
|
6610
|
+
};
|
|
6611
|
+
|
|
6612
|
+
/**
|
|
6613
|
+
* Extract outline (function/class signatures).
|
|
6614
|
+
* @param file - File object with .lines array
|
|
6615
|
+
*/
|
|
6616
|
+
globalThis.outline = (file) => {
|
|
6617
|
+
const lines = file?.lines;
|
|
6618
|
+
if (!Array.isArray(lines)) return [];
|
|
6619
|
+
const signatures = [];
|
|
6620
|
+
const patterns = [
|
|
6621
|
+
/^(export\\s+)?(async\\s+)?function\\s+\\w+/,
|
|
6622
|
+
/^(export\\s+)?(const|let|var)\\s+\\w+\\s*=\\s*(async\\s+)?\\(/,
|
|
6623
|
+
/^(export\\s+)?(const|let|var)\\s+\\w+\\s*=\\s*(async\\s+)?function/,
|
|
6624
|
+
/^(export\\s+)?class\\s+\\w+/,
|
|
6625
|
+
/^(export\\s+)?interface\\s+\\w+/,
|
|
6626
|
+
/^(export\\s+)?type\\s+\\w+/,
|
|
6627
|
+
/^\\s+(async\\s+)?\\w+\\s*\\([^)]*\\)\\s*[:{]/,
|
|
6628
|
+
];
|
|
6629
|
+
|
|
6630
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6631
|
+
const line = lines[i];
|
|
6632
|
+
for (const pat of patterns) {
|
|
6633
|
+
if (pat.test(line)) {
|
|
6634
|
+
signatures.push((i + 1) + ': ' + line.trim().slice(0, 80));
|
|
6635
|
+
break;
|
|
6636
|
+
}
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6639
|
+
return signatures;
|
|
6640
|
+
};
|
|
6641
|
+
|
|
6491
6642
|
// SECURITY: Reserved keys that must not be overwritten by user-provided variables/globals
|
|
6492
6643
|
const RESERVED_KEYS = new Set([
|
|
6493
6644
|
'onmessage', 'postMessage', 'close', 'terminate', 'self',
|
|
6494
6645
|
'constructor', 'prototype', '__proto__',
|
|
6495
6646
|
'pendingCalls', 'callId', 'logs', 'console', 'adapters',
|
|
6496
6647
|
'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',
|
|
6497
|
-
'pick', 'table', 'count', 'sum', 'first', 'safeStr'
|
|
6648
|
+
'pick', 'table', 'count', 'sum', 'first', 'safeStr', 'poll', 'waitFor',
|
|
6649
|
+
'around', 'block', 'grep', 'outline'
|
|
6498
6650
|
]);
|
|
6499
6651
|
|
|
6500
6652
|
self.onmessage = async (event) => {
|
|
@@ -6538,6 +6690,12 @@ class BunWorkerSandbox {
|
|
|
6538
6690
|
return matrix[b.length][a.length];
|
|
6539
6691
|
};
|
|
6540
6692
|
|
|
6693
|
+
// Convert camelCase to snake_case
|
|
6694
|
+
const toSnakeCase = (str) => str
|
|
6695
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
6696
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
6697
|
+
.toLowerCase();
|
|
6698
|
+
|
|
6541
6699
|
// Find similar method names
|
|
6542
6700
|
const findSimilar = (name, methods, maxDist = 3) => {
|
|
6543
6701
|
const normalized = name.toLowerCase().replace(/[-_]/g, '');
|
|
@@ -6549,6 +6707,22 @@ class BunWorkerSandbox {
|
|
|
6549
6707
|
.map(x => x.method);
|
|
6550
6708
|
};
|
|
6551
6709
|
|
|
6710
|
+
// Find best auto-correctable match (distance ≤ 2 = safe to auto-correct)
|
|
6711
|
+
const findAutoCorrect = (name, methods) => {
|
|
6712
|
+
// Try exact snake_case conversion first
|
|
6713
|
+
const snake = toSnakeCase(name);
|
|
6714
|
+
if (methods.includes(snake)) return snake;
|
|
6715
|
+
|
|
6716
|
+
// Try normalized fuzzy match
|
|
6717
|
+
const normalized = name.toLowerCase().replace(/[-_]/g, '');
|
|
6718
|
+
const matches = methods
|
|
6719
|
+
.map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
|
|
6720
|
+
.filter(x => x.dist <= 2) // Only auto-correct if very close
|
|
6721
|
+
.sort((a, b) => a.dist - b.dist);
|
|
6722
|
+
|
|
6723
|
+
return matches.length > 0 ? matches[0].method : null;
|
|
6724
|
+
};
|
|
6725
|
+
|
|
6552
6726
|
// Create adapter proxies with helpful error messages
|
|
6553
6727
|
const adaptersObj = {};
|
|
6554
6728
|
for (const [adapterName, methods] of Object.entries(adapterMethods)) {
|
|
@@ -6569,16 +6743,45 @@ class BunWorkerSandbox {
|
|
|
6569
6743
|
};
|
|
6570
6744
|
}
|
|
6571
6745
|
|
|
6572
|
-
//
|
|
6746
|
+
// Precompute auto-correct lookup (O(1) instead of O(n) per access)
|
|
6747
|
+
const autoCorrectMap = new Map();
|
|
6748
|
+
for (const m of methods) {
|
|
6749
|
+
// Map snake_case variants: execute_sql -> execute_sql (identity)
|
|
6750
|
+
autoCorrectMap.set(m, m);
|
|
6751
|
+
// Map normalized: executesql -> execute_sql
|
|
6752
|
+
autoCorrectMap.set(m.toLowerCase().replace(/[-_]/g, ''), m);
|
|
6753
|
+
}
|
|
6754
|
+
|
|
6755
|
+
// Use Proxy to intercept undefined method calls with auto-correction
|
|
6573
6756
|
const adapterProxy = new Proxy(methodsImpl, {
|
|
6574
6757
|
get(target, prop) {
|
|
6575
6758
|
if (prop in target) return target[prop];
|
|
6576
6759
|
if (typeof prop === 'symbol') return undefined;
|
|
6577
|
-
|
|
6760
|
+
|
|
6761
|
+
const propStr = String(prop);
|
|
6762
|
+
|
|
6763
|
+
// Fast path: check precomputed map (O(1))
|
|
6764
|
+
const snake = toSnakeCase(propStr);
|
|
6765
|
+
if (autoCorrectMap.has(snake)) {
|
|
6766
|
+
return target[autoCorrectMap.get(snake)];
|
|
6767
|
+
}
|
|
6768
|
+
const normalized = propStr.toLowerCase().replace(/[-_]/g, '');
|
|
6769
|
+
if (autoCorrectMap.has(normalized)) {
|
|
6770
|
+
return target[autoCorrectMap.get(normalized)];
|
|
6771
|
+
}
|
|
6772
|
+
|
|
6773
|
+
// Slow path: fuzzy match for typos (only if fast path failed)
|
|
6774
|
+
const corrected = findAutoCorrect(propStr, methods);
|
|
6775
|
+
if (corrected && corrected in target) {
|
|
6776
|
+
return target[corrected];
|
|
6777
|
+
}
|
|
6778
|
+
|
|
6779
|
+
// No auto-correct possible, throw with suggestions
|
|
6780
|
+
const similar = findSimilar(propStr, methods);
|
|
6578
6781
|
const suggestion = similar.length > 0
|
|
6579
6782
|
? '. Did you mean: ' + similar.join(', ') + '?'
|
|
6580
6783
|
: '. Available: ' + methods.slice(0, 5).join(', ') + (methods.length > 5 ? '...' : '');
|
|
6581
|
-
throw new Error(adapterName + '.' +
|
|
6784
|
+
throw new Error(adapterName + '.' + propStr + ' is not a function' + suggestion);
|
|
6582
6785
|
}
|
|
6583
6786
|
});
|
|
6584
6787
|
|
|
@@ -6615,7 +6818,23 @@ class BunWorkerSandbox {
|
|
|
6615
6818
|
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
6616
6819
|
const fn = new AsyncFunction(data.code);
|
|
6617
6820
|
const result = await fn();
|
|
6618
|
-
|
|
6821
|
+
|
|
6822
|
+
// SECURITY: Hard cap on output size to prevent OOM
|
|
6823
|
+
const HARD_CAP_BYTES = 100 * 1024 * 1024; // 100MB
|
|
6824
|
+
const serialized = safeStr(result);
|
|
6825
|
+
if (serialized.length > HARD_CAP_BYTES) {
|
|
6826
|
+
self.postMessage({
|
|
6827
|
+
type: 'result',
|
|
6828
|
+
success: false,
|
|
6829
|
+
error: {
|
|
6830
|
+
name: 'OutputSizeError',
|
|
6831
|
+
message: 'Output exceeds 100MB limit. Use pagination or filtering.'
|
|
6832
|
+
},
|
|
6833
|
+
logs
|
|
6834
|
+
});
|
|
6835
|
+
} else {
|
|
6836
|
+
self.postMessage({ type: 'result', success: true, value: result, logs });
|
|
6837
|
+
}
|
|
6619
6838
|
} catch (err) {
|
|
6620
6839
|
// Truncate stack to 5 lines to prevent context bloat
|
|
6621
6840
|
const stack = err.stack ? err.stack.split('\\n').slice(0, 5).join('\\n') : undefined;
|
package/package.json
CHANGED
|
@@ -285,13 +285,165 @@ 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
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get lines around a specific line number (1-indexed).
|
|
339
|
+
* @param file - File object with .lines array
|
|
340
|
+
* @param line - Line number (1-indexed)
|
|
341
|
+
* @param ctx - Context lines before/after (default 10)
|
|
342
|
+
*/
|
|
343
|
+
globalThis.around = (file, line, ctx = 10) => {
|
|
344
|
+
const lines = file?.lines;
|
|
345
|
+
if (!Array.isArray(lines)) return [];
|
|
346
|
+
if (line < 1 || line > lines.length) return [];
|
|
347
|
+
const idx = line - 1;
|
|
348
|
+
return lines.slice(Math.max(0, idx - ctx), Math.min(lines.length, idx + ctx + 1));
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Extract code block containing a line (detects by indentation).
|
|
353
|
+
* @param file - File object with .lines array
|
|
354
|
+
* @param line - Line number (1-indexed)
|
|
355
|
+
*/
|
|
356
|
+
globalThis.block = (file, line) => {
|
|
357
|
+
const lines = file?.lines;
|
|
358
|
+
if (!Array.isArray(lines)) return [];
|
|
359
|
+
const idx = line - 1;
|
|
360
|
+
if (idx < 0 || idx >= lines.length) return [];
|
|
361
|
+
|
|
362
|
+
const targetIndent = lines[idx].search(/\\S/);
|
|
363
|
+
if (targetIndent < 0) return [lines[idx]];
|
|
364
|
+
|
|
365
|
+
// Find block start
|
|
366
|
+
let start = idx;
|
|
367
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
368
|
+
const lineIndent = lines[i].search(/\\S/);
|
|
369
|
+
if (lineIndent >= 0 && lineIndent < targetIndent) { start = i; break; }
|
|
370
|
+
if (lineIndent === 0 && lines[i].trim()) { start = i; break; }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Find block end
|
|
374
|
+
const startIndent = lines[start].search(/\\S/);
|
|
375
|
+
let end = idx;
|
|
376
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
377
|
+
const lineIndent = lines[i].search(/\\S/);
|
|
378
|
+
if (lineIndent >= 0 && lineIndent <= startIndent && lines[i].trim()) { end = i - 1; break; }
|
|
379
|
+
end = i;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return lines.slice(start, end + 1);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Grep with context lines.
|
|
387
|
+
* @param file - File object with .lines array
|
|
388
|
+
* @param pattern - String or regex pattern
|
|
389
|
+
* @param ctx - Context lines (default 3)
|
|
390
|
+
*/
|
|
391
|
+
globalThis.grep = (file, pattern, ctx = 3) => {
|
|
392
|
+
const lines = file?.lines;
|
|
393
|
+
if (!Array.isArray(lines)) return [];
|
|
394
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern, 'i') : pattern;
|
|
395
|
+
const results = [];
|
|
396
|
+
|
|
397
|
+
for (let i = 0; i < lines.length; i++) {
|
|
398
|
+
if (regex.test(lines[i])) {
|
|
399
|
+
results.push({
|
|
400
|
+
line: i + 1,
|
|
401
|
+
match: lines[i],
|
|
402
|
+
context: lines.slice(Math.max(0, i - ctx), i + ctx + 1)
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return results;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Extract outline (function/class signatures).
|
|
411
|
+
* @param file - File object with .lines array
|
|
412
|
+
*/
|
|
413
|
+
globalThis.outline = (file) => {
|
|
414
|
+
const lines = file?.lines;
|
|
415
|
+
if (!Array.isArray(lines)) return [];
|
|
416
|
+
const signatures = [];
|
|
417
|
+
const patterns = [
|
|
418
|
+
/^(export\\s+)?(async\\s+)?function\\s+\\w+/,
|
|
419
|
+
/^(export\\s+)?(const|let|var)\\s+\\w+\\s*=\\s*(async\\s+)?\\(/,
|
|
420
|
+
/^(export\\s+)?(const|let|var)\\s+\\w+\\s*=\\s*(async\\s+)?function/,
|
|
421
|
+
/^(export\\s+)?class\\s+\\w+/,
|
|
422
|
+
/^(export\\s+)?interface\\s+\\w+/,
|
|
423
|
+
/^(export\\s+)?type\\s+\\w+/,
|
|
424
|
+
/^\\s+(async\\s+)?\\w+\\s*\\([^)]*\\)\\s*[:{]/,
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
for (let i = 0; i < lines.length; i++) {
|
|
428
|
+
const line = lines[i];
|
|
429
|
+
for (const pat of patterns) {
|
|
430
|
+
if (pat.test(line)) {
|
|
431
|
+
signatures.push((i + 1) + ': ' + line.trim().slice(0, 80));
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return signatures;
|
|
437
|
+
};
|
|
438
|
+
|
|
288
439
|
// SECURITY: Reserved keys that must not be overwritten by user-provided variables/globals
|
|
289
440
|
const RESERVED_KEYS = new Set([
|
|
290
441
|
'onmessage', 'postMessage', 'close', 'terminate', 'self',
|
|
291
442
|
'constructor', 'prototype', '__proto__',
|
|
292
443
|
'pendingCalls', 'callId', 'logs', 'console', 'adapters',
|
|
293
444
|
'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',
|
|
294
|
-
'pick', 'table', 'count', 'sum', 'first', 'safeStr'
|
|
445
|
+
'pick', 'table', 'count', 'sum', 'first', 'safeStr', 'poll', 'waitFor',
|
|
446
|
+
'around', 'block', 'grep', 'outline'
|
|
295
447
|
]);
|
|
296
448
|
|
|
297
449
|
self.onmessage = async (event) => {
|
|
@@ -335,6 +487,12 @@ export class BunWorkerSandbox implements ISandbox {
|
|
|
335
487
|
return matrix[b.length][a.length];
|
|
336
488
|
};
|
|
337
489
|
|
|
490
|
+
// Convert camelCase to snake_case
|
|
491
|
+
const toSnakeCase = (str) => str
|
|
492
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
493
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
494
|
+
.toLowerCase();
|
|
495
|
+
|
|
338
496
|
// Find similar method names
|
|
339
497
|
const findSimilar = (name, methods, maxDist = 3) => {
|
|
340
498
|
const normalized = name.toLowerCase().replace(/[-_]/g, '');
|
|
@@ -346,6 +504,22 @@ export class BunWorkerSandbox implements ISandbox {
|
|
|
346
504
|
.map(x => x.method);
|
|
347
505
|
};
|
|
348
506
|
|
|
507
|
+
// Find best auto-correctable match (distance ≤ 2 = safe to auto-correct)
|
|
508
|
+
const findAutoCorrect = (name, methods) => {
|
|
509
|
+
// Try exact snake_case conversion first
|
|
510
|
+
const snake = toSnakeCase(name);
|
|
511
|
+
if (methods.includes(snake)) return snake;
|
|
512
|
+
|
|
513
|
+
// Try normalized fuzzy match
|
|
514
|
+
const normalized = name.toLowerCase().replace(/[-_]/g, '');
|
|
515
|
+
const matches = methods
|
|
516
|
+
.map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
|
|
517
|
+
.filter(x => x.dist <= 2) // Only auto-correct if very close
|
|
518
|
+
.sort((a, b) => a.dist - b.dist);
|
|
519
|
+
|
|
520
|
+
return matches.length > 0 ? matches[0].method : null;
|
|
521
|
+
};
|
|
522
|
+
|
|
349
523
|
// Create adapter proxies with helpful error messages
|
|
350
524
|
const adaptersObj = {};
|
|
351
525
|
for (const [adapterName, methods] of Object.entries(adapterMethods)) {
|
|
@@ -366,16 +540,45 @@ export class BunWorkerSandbox implements ISandbox {
|
|
|
366
540
|
};
|
|
367
541
|
}
|
|
368
542
|
|
|
369
|
-
//
|
|
543
|
+
// Precompute auto-correct lookup (O(1) instead of O(n) per access)
|
|
544
|
+
const autoCorrectMap = new Map();
|
|
545
|
+
for (const m of methods) {
|
|
546
|
+
// Map snake_case variants: execute_sql -> execute_sql (identity)
|
|
547
|
+
autoCorrectMap.set(m, m);
|
|
548
|
+
// Map normalized: executesql -> execute_sql
|
|
549
|
+
autoCorrectMap.set(m.toLowerCase().replace(/[-_]/g, ''), m);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Use Proxy to intercept undefined method calls with auto-correction
|
|
370
553
|
const adapterProxy = new Proxy(methodsImpl, {
|
|
371
554
|
get(target, prop) {
|
|
372
555
|
if (prop in target) return target[prop];
|
|
373
556
|
if (typeof prop === 'symbol') return undefined;
|
|
374
|
-
|
|
557
|
+
|
|
558
|
+
const propStr = String(prop);
|
|
559
|
+
|
|
560
|
+
// Fast path: check precomputed map (O(1))
|
|
561
|
+
const snake = toSnakeCase(propStr);
|
|
562
|
+
if (autoCorrectMap.has(snake)) {
|
|
563
|
+
return target[autoCorrectMap.get(snake)];
|
|
564
|
+
}
|
|
565
|
+
const normalized = propStr.toLowerCase().replace(/[-_]/g, '');
|
|
566
|
+
if (autoCorrectMap.has(normalized)) {
|
|
567
|
+
return target[autoCorrectMap.get(normalized)];
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Slow path: fuzzy match for typos (only if fast path failed)
|
|
571
|
+
const corrected = findAutoCorrect(propStr, methods);
|
|
572
|
+
if (corrected && corrected in target) {
|
|
573
|
+
return target[corrected];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// No auto-correct possible, throw with suggestions
|
|
577
|
+
const similar = findSimilar(propStr, methods);
|
|
375
578
|
const suggestion = similar.length > 0
|
|
376
579
|
? '. Did you mean: ' + similar.join(', ') + '?'
|
|
377
580
|
: '. Available: ' + methods.slice(0, 5).join(', ') + (methods.length > 5 ? '...' : '');
|
|
378
|
-
throw new Error(adapterName + '.' +
|
|
581
|
+
throw new Error(adapterName + '.' + propStr + ' is not a function' + suggestion);
|
|
379
582
|
}
|
|
380
583
|
});
|
|
381
584
|
|
|
@@ -412,7 +615,23 @@ export class BunWorkerSandbox implements ISandbox {
|
|
|
412
615
|
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
413
616
|
const fn = new AsyncFunction(data.code);
|
|
414
617
|
const result = await fn();
|
|
415
|
-
|
|
618
|
+
|
|
619
|
+
// SECURITY: Hard cap on output size to prevent OOM
|
|
620
|
+
const HARD_CAP_BYTES = 100 * 1024 * 1024; // 100MB
|
|
621
|
+
const serialized = safeStr(result);
|
|
622
|
+
if (serialized.length > HARD_CAP_BYTES) {
|
|
623
|
+
self.postMessage({
|
|
624
|
+
type: 'result',
|
|
625
|
+
success: false,
|
|
626
|
+
error: {
|
|
627
|
+
name: 'OutputSizeError',
|
|
628
|
+
message: 'Output exceeds 100MB limit. Use pagination or filtering.'
|
|
629
|
+
},
|
|
630
|
+
logs
|
|
631
|
+
});
|
|
632
|
+
} else {
|
|
633
|
+
self.postMessage({ type: 'result', success: true, value: result, logs });
|
|
634
|
+
}
|
|
416
635
|
} catch (err) {
|
|
417
636
|
// Truncate stack to 5 lines to prevent context bloat
|
|
418
637
|
const stack = err.stack ? err.stack.split('\\n').slice(0, 5).join('\\n') : undefined;
|