Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35d562bb09 | |||
| 0a3f10cbc0 | |||
|
|
0e585e492a |
54
electron-main-secure.js
Normal file
54
electron-main-secure.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
// Import the secure MemPalace bridge
|
||||
const { setupSecureMemPalaceIPC } = require('./electron-mempalace-bridge');
|
||||
|
||||
let mainWindow;
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.loadFile('index.html');
|
||||
|
||||
// Open DevTools in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
// Set up secure MemPalace IPC
|
||||
setupSecureMemPalaceIPC();
|
||||
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle any uncaught exceptions
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
290
electron-mempalace-bridge.js
Normal file
290
electron-mempalace-bridge.js
Normal file
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* Secure MemPalace IPC Bridge
|
||||
* Issue #1423: [SECURITY] Electron MemPalace bridge allows arbitrary command execution
|
||||
*
|
||||
* Replaces raw command execution with typed, validated IPC actions.
|
||||
*/
|
||||
|
||||
const { app, BrowserWindow, ipcMain } = require('electron');
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
// Whitelist of allowed MemPalace actions
|
||||
const ALLOWED_ACTIONS = {
|
||||
'init': {
|
||||
command: 'mempalace',
|
||||
args: ['init'],
|
||||
requiredArgs: ['palacePath'],
|
||||
validate: (args) => {
|
||||
// Validate palacePath is safe (no shell metacharacters)
|
||||
const palacePath = args.palacePath;
|
||||
if (!palacePath || typeof palacePath !== 'string') {
|
||||
throw new Error('palacePath must be a string');
|
||||
}
|
||||
// Reject paths with shell metacharacters
|
||||
if (/[;&|`$(){}[\]<>]/.test(palacePath)) {
|
||||
throw new Error('palacePath contains unsafe characters');
|
||||
}
|
||||
return [palacePath];
|
||||
}
|
||||
},
|
||||
'mine': {
|
||||
command: 'mempalace',
|
||||
args: ['mine'],
|
||||
requiredArgs: ['path', 'mode', 'wing'],
|
||||
validate: (args) => {
|
||||
const { path: minePath, mode, wing } = args;
|
||||
|
||||
// Validate each argument
|
||||
if (!minePath || typeof minePath !== 'string') {
|
||||
throw new Error('path must be a string');
|
||||
}
|
||||
if (!mode || typeof mode !== 'string') {
|
||||
throw new Error('mode must be a string');
|
||||
}
|
||||
if (!wing || typeof wing !== 'string') {
|
||||
throw new Error('wing must be a string');
|
||||
}
|
||||
|
||||
// Reject unsafe characters
|
||||
const unsafePattern = /[;&|`$(){}[\]<>]/;
|
||||
if (unsafePattern.test(minePath) || unsafePattern.test(mode) || unsafePattern.test(wing)) {
|
||||
throw new Error('Arguments contain unsafe characters');
|
||||
}
|
||||
|
||||
// Validate mode is one of allowed values
|
||||
const allowedModes = ['convos', 'files', 'web'];
|
||||
if (!allowedModes.includes(mode)) {
|
||||
throw new Error(`Mode must be one of: ${allowedModes.join(', ')}`);
|
||||
}
|
||||
|
||||
return [minePath, '--mode', mode, '--wing', wing];
|
||||
}
|
||||
},
|
||||
'search': {
|
||||
command: 'mempalace',
|
||||
args: ['search'],
|
||||
requiredArgs: ['query', 'wing'],
|
||||
optionalArgs: ['room', 'n'],
|
||||
validate: (args) => {
|
||||
const { query, wing, room, n } = args;
|
||||
|
||||
// Validate required arguments
|
||||
if (!query || typeof query !== 'string') {
|
||||
throw new Error('query must be a string');
|
||||
}
|
||||
if (!wing || typeof wing !== 'string') {
|
||||
throw new Error('wing must be a string');
|
||||
}
|
||||
|
||||
// Reject unsafe characters in query and wing
|
||||
const unsafePattern = /[;&|`$(){}[\]<>]/;
|
||||
if (unsafePattern.test(query) || unsafePattern.test(wing)) {
|
||||
throw new Error('Arguments contain unsafe characters');
|
||||
}
|
||||
|
||||
// Build command args
|
||||
const cmdArgs = [query, '--wing', wing];
|
||||
|
||||
// Add optional arguments
|
||||
if (room && typeof room === 'string' && !unsafePattern.test(room)) {
|
||||
cmdArgs.push('--room', room);
|
||||
}
|
||||
if (n && typeof n === 'number' && n > 0 && n <= 100) {
|
||||
cmdArgs.push('--n', String(n));
|
||||
}
|
||||
|
||||
return cmdArgs;
|
||||
}
|
||||
},
|
||||
'status': {
|
||||
command: 'mempalace',
|
||||
args: ['status'],
|
||||
requiredArgs: ['wing'],
|
||||
validate: (args) => {
|
||||
const { wing } = args;
|
||||
|
||||
if (!wing || typeof wing !== 'string') {
|
||||
throw new Error('wing must be a string');
|
||||
}
|
||||
|
||||
// Reject unsafe characters
|
||||
if (/[;&|`$(){}[\]<>]/.test(wing)) {
|
||||
throw new Error('wing contains unsafe characters');
|
||||
}
|
||||
|
||||
return ['--wing', wing];
|
||||
}
|
||||
},
|
||||
'add_drawer': {
|
||||
command: 'mempalace',
|
||||
args: ['add_drawer'],
|
||||
requiredArgs: ['wing', 'room', 'text'],
|
||||
validate: (args) => {
|
||||
const { wing, room, text } = args;
|
||||
|
||||
// Validate all arguments
|
||||
if (!wing || typeof wing !== 'string') {
|
||||
throw new Error('wing must be a string');
|
||||
}
|
||||
if (!room || typeof room !== 'string') {
|
||||
throw new Error('room must be a string');
|
||||
}
|
||||
if (!text || typeof text !== 'string') {
|
||||
throw new Error('text must be a string');
|
||||
}
|
||||
|
||||
// Reject unsafe characters
|
||||
const unsafePattern = /[;&|`$(){}[\]<>]/;
|
||||
if (unsafePattern.test(wing) || unsafePattern.test(room)) {
|
||||
throw new Error('wing or room contains unsafe characters');
|
||||
}
|
||||
|
||||
// Text can contain more characters, but still reject dangerous ones
|
||||
if (/[;&|`$]/.test(text)) {
|
||||
throw new Error('text contains unsafe characters');
|
||||
}
|
||||
|
||||
return [wing, room, text];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate and execute a MemPalace action
|
||||
*/
|
||||
async function executeMemPalaceAction(action, args = {}) {
|
||||
// Check if action is allowed
|
||||
if (!ALLOWED_ACTIONS[action]) {
|
||||
throw new Error(`Unknown action: ${action}. Allowed actions: ${Object.keys(ALLOWED_ACTIONS).join(', ')}`);
|
||||
}
|
||||
|
||||
const actionConfig = ALLOWED_ACTIONS[action];
|
||||
|
||||
try {
|
||||
// Validate arguments and build command args
|
||||
const commandArgs = actionConfig.validate(args);
|
||||
|
||||
// Build full command
|
||||
const command = actionConfig.command;
|
||||
const fullArgs = [...actionConfig.args, ...commandArgs];
|
||||
|
||||
console.log(`[MemPalace] Executing: ${command} ${fullArgs.join(' ')}`);
|
||||
|
||||
// Execute with spawn (safer than exec)
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, fullArgs, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: false // Don't use shell
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr, code });
|
||||
} else {
|
||||
reject(new Error(`Command failed with code ${code}: ${stderr}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[MemPalace] Validation error for ${action}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up secure IPC handlers
|
||||
*/
|
||||
function setupSecureMemPalaceIPC() {
|
||||
// Remove any existing handlers
|
||||
ipcMain.removeHandler('exec-python');
|
||||
|
||||
// Set up typed action handlers
|
||||
ipcMain.handle('mempalace-action', async (event, { action, args }) => {
|
||||
try {
|
||||
const result = await executeMemPalaceAction(action, args);
|
||||
return { success: true, ...result };
|
||||
} catch (error) {
|
||||
console.error(`[MemPalace] Action ${action} failed:`, error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Keep legacy exec-python handler but with validation (for backward compatibility)
|
||||
// This should be deprecated and removed in future versions
|
||||
ipcMain.handle('exec-python', async (event, command) => {
|
||||
console.warn('[MemPalace] DEPRECATED: exec-python called. Use mempalace-action instead.');
|
||||
|
||||
// Parse the command to extract action and args
|
||||
const parts = command.trim().split(/\s+/);
|
||||
if (parts.length < 2 || parts[0] !== 'mempalace') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Only mempalace commands are allowed',
|
||||
deprecated: true
|
||||
};
|
||||
}
|
||||
|
||||
const action = parts[1];
|
||||
const args = {};
|
||||
|
||||
// Parse arguments from command string
|
||||
// This is a simplified parser - in production, use proper argument parsing
|
||||
for (let i = 2; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (part.startsWith('--')) {
|
||||
const key = part.slice(2);
|
||||
const value = parts[i + 1];
|
||||
if (value && !value.startsWith('--')) {
|
||||
args[key] = value;
|
||||
i++; // Skip next part
|
||||
}
|
||||
} else if (!args.path && !args.wing && !args.query) {
|
||||
// Positional arguments
|
||||
if (!args.path) args.path = part;
|
||||
else if (!args.wing) args.wing = part;
|
||||
else if (!args.query) args.query = part;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await executeMemPalaceAction(action, args);
|
||||
return {
|
||||
success: true,
|
||||
...result,
|
||||
deprecated: true,
|
||||
warning: 'This endpoint is deprecated. Use mempalace-action instead.'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
deprecated: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[MemPalace] Secure IPC handlers registered');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupSecureMemPalaceIPC,
|
||||
executeMemPalaceAction,
|
||||
ALLOWED_ACTIONS
|
||||
};
|
||||
@@ -1,317 +0,0 @@
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// SPATIAL SEARCH — Find nearest user/object by name
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
//
|
||||
// Search for users/objects by name with distance and direction.
|
||||
// Provides autocomplete, pathfinding arrow, and keyboard shortcuts.
|
||||
//
|
||||
// Usage:
|
||||
// const search = new SpatialSearch({ maxDistance: 1000 });
|
||||
// search.registerEntity('id', { name, type, position });
|
||||
// const results = search.searchEntities('query');
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class SpatialSearch {
|
||||
constructor(options = {}) {
|
||||
this.maxDistance = options.maxDistance || 1000;
|
||||
this.onResultSelect = options.onResultSelect || null;
|
||||
this.entities = new Map();
|
||||
this.selectedIndex = -1;
|
||||
this.results = [];
|
||||
this.isOpen = false;
|
||||
this._initUI();
|
||||
this._bindKeys();
|
||||
}
|
||||
|
||||
// ─── Entity Management ─────────────────────────────────
|
||||
|
||||
registerEntity(id, { name, type = 'object', position }) {
|
||||
this.entities.set(id, {
|
||||
id,
|
||||
name: name.toLowerCase(),
|
||||
displayName: name,
|
||||
type,
|
||||
position: { ...position }
|
||||
});
|
||||
}
|
||||
|
||||
unregisterEntity(id) {
|
||||
this.entities.delete(id);
|
||||
}
|
||||
|
||||
updateEntityPosition(id, position) {
|
||||
const entity = this.entities.get(id);
|
||||
if (entity) {
|
||||
entity.position = { ...position };
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Search ────────────────────────────────────────────
|
||||
|
||||
searchEntities(query, cameraPosition = null) {
|
||||
if (!query || query.length < 1) {
|
||||
this.results = [];
|
||||
this._renderResults();
|
||||
return [];
|
||||
}
|
||||
|
||||
const q = query.toLowerCase();
|
||||
const results = [];
|
||||
|
||||
for (const [id, entity] of this.entities) {
|
||||
if (!entity.name.includes(q)) continue;
|
||||
|
||||
let distance = 0;
|
||||
let direction = '';
|
||||
|
||||
if (cameraPosition) {
|
||||
distance = this._calculateDistance(cameraPosition, entity.position);
|
||||
if (distance > this.maxDistance) continue;
|
||||
direction = this._calculateDirection(cameraPosition, entity.position);
|
||||
}
|
||||
|
||||
results.push({
|
||||
id,
|
||||
name: entity.displayName,
|
||||
type: entity.type,
|
||||
distance: Math.round(distance * 10) / 10,
|
||||
direction,
|
||||
position: entity.position
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by distance
|
||||
results.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
this.results = results.slice(0, 10); // Limit to 10 results
|
||||
this.selectedIndex = this.results.length > 0 ? 0 : -1;
|
||||
this._renderResults();
|
||||
|
||||
return this.results;
|
||||
}
|
||||
|
||||
selectResult(index) {
|
||||
if (index < 0 || index >= this.results.length) return;
|
||||
|
||||
this.selectedIndex = index;
|
||||
this._renderResults();
|
||||
|
||||
const result = this.results[index];
|
||||
if (result && this.onResultSelect) {
|
||||
this.onResultSelect(result);
|
||||
}
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
||||
// ─── Distance & Direction ──────────────────────────────
|
||||
|
||||
_calculateDistance(from, to) {
|
||||
const dx = to.x - from.x;
|
||||
const dy = to.y - from.y;
|
||||
const dz = to.z - from.z;
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
_calculateDirection(from, to) {
|
||||
const dx = to.x - from.x;
|
||||
const dz = to.z - from.z;
|
||||
const angle = Math.atan2(dx, dz) * (180 / Math.PI);
|
||||
|
||||
// Convert to compass direction
|
||||
if (angle >= -22.5 && angle < 22.5) return 'N';
|
||||
if (angle >= 22.5 && angle < 67.5) return 'NE';
|
||||
if (angle >= 67.5 && angle < 112.5) return 'E';
|
||||
if (angle >= 112.5 && angle < 157.5) return 'SE';
|
||||
if (angle >= 157.5 || angle < -157.5) return 'S';
|
||||
if (angle >= -157.5 && angle < -112.5) return 'SW';
|
||||
if (angle >= -112.5 && angle < -67.5) return 'W';
|
||||
if (angle >= -67.5 && angle < -22.5) return 'NW';
|
||||
return 'N';
|
||||
}
|
||||
|
||||
// ─── UI ────────────────────────────────────────────────
|
||||
|
||||
_initUI() {
|
||||
// Search container
|
||||
this.container = document.createElement('div');
|
||||
this.container.id = 'spatial-search';
|
||||
this.container.className = 'spatial-search';
|
||||
this.container.style.display = 'none';
|
||||
|
||||
// Input
|
||||
this.input = document.createElement('input');
|
||||
this.input.type = 'text';
|
||||
this.input.className = 'spatial-search-input';
|
||||
this.input.placeholder = 'Search by name... (Ctrl+F)';
|
||||
this.input.addEventListener('input', () => this._onInput());
|
||||
this.input.addEventListener('keydown', (e) => this._onKeyDown(e));
|
||||
|
||||
// Results dropdown
|
||||
this.dropdown = document.createElement('div');
|
||||
this.dropdown.className = 'spatial-search-dropdown';
|
||||
|
||||
// Path arrow
|
||||
this.arrow = document.createElement('div');
|
||||
this.arrow.className = 'spatial-search-arrow';
|
||||
this.arrow.style.display = 'none';
|
||||
this.arrow.innerHTML = '<span class="arrow-icon">➤</span><span class="arrow-info"></span>';
|
||||
|
||||
this.container.appendChild(this.input);
|
||||
this.container.appendChild(this.dropdown);
|
||||
|
||||
document.body.appendChild(this.container);
|
||||
document.body.appendChild(this.arrow);
|
||||
}
|
||||
|
||||
_bindKeys() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Ctrl+F or Cmd+F to toggle
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
||||
e.preventDefault();
|
||||
this.toggle();
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape to close
|
||||
if (e.key === 'Escape' && this.isOpen) {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_onInput() {
|
||||
const query = this.input.value;
|
||||
// Get camera position if available
|
||||
const cameraPos = window.camera ? {
|
||||
x: window.camera.position.x,
|
||||
y: window.camera.position.y,
|
||||
z: window.camera.position.z
|
||||
} : null;
|
||||
|
||||
this.searchEntities(query, cameraPos);
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
if (!this.isOpen || this.results.length === 0) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.min(this.selectedIndex + 1, this.results.length - 1);
|
||||
this._renderResults();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
||||
this._renderResults();
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (this.selectedIndex >= 0) {
|
||||
this.selectResult(this.selectedIndex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_renderResults() {
|
||||
this.dropdown.innerHTML = '';
|
||||
|
||||
if (this.results.length === 0) {
|
||||
if (this.input.value) {
|
||||
this.dropdown.innerHTML = '<div class="spatial-search-empty">No results found</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.results.forEach((result, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = `spatial-search-item ${index === this.selectedIndex ? 'selected' : ''}`;
|
||||
item.innerHTML = `
|
||||
<span class="item-name">${this._escapeHtml(result.name)}</span>
|
||||
<span class="item-type">${result.type}</span>
|
||||
<span class="item-distance">${result.distance}m ${result.direction}</span>
|
||||
`;
|
||||
item.addEventListener('click', () => this.selectResult(index));
|
||||
this.dropdown.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
_updateArrow(result) {
|
||||
if (!result) {
|
||||
this.arrow.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
this.arrow.style.display = 'flex';
|
||||
const info = this.arrow.querySelector('.arrow-info');
|
||||
if (info) {
|
||||
info.textContent = `${result.name} — ${result.distance}m ${result.direction}`;
|
||||
}
|
||||
|
||||
// Rotate arrow based on direction
|
||||
const rotations = {
|
||||
'N': 0, 'NE': 45, 'E': 90, 'SE': 135,
|
||||
'S': 180, 'SW': 225, 'W': 270, 'NW': 315
|
||||
};
|
||||
const arrowIcon = this.arrow.querySelector('.arrow-icon');
|
||||
if (arrowIcon) {
|
||||
arrowIcon.style.transform = `rotate(${rotations[result.direction] || 0}deg)`;
|
||||
}
|
||||
}
|
||||
|
||||
_escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ─── Public API ────────────────────────────────────────
|
||||
|
||||
open() {
|
||||
this.isOpen = true;
|
||||
this.container.style.display = 'flex';
|
||||
this.input.focus();
|
||||
this.input.value = '';
|
||||
this.results = [];
|
||||
this._renderResults();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.container.style.display = 'none';
|
||||
this.input.blur();
|
||||
this.selectedIndex = -1;
|
||||
this._updateArrow(null);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.isOpen) {
|
||||
this.close();
|
||||
} else {
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
entityCount: this.entities.size,
|
||||
isOpen: this.isOpen,
|
||||
resultCount: this.results.length,
|
||||
selectedIndex: this.selectedIndex
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module systems
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = SpatialSearch;
|
||||
}
|
||||
|
||||
// Make available globally
|
||||
if (typeof window !== 'undefined') {
|
||||
window.SpatialSearch = SpatialSearch;
|
||||
}
|
||||
24
preload.js
Normal file
24
preload.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Preload script for Electron
|
||||
* Exposes secure MemPalace API to renderer
|
||||
*/
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
// Expose secure MemPalace API to renderer
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Secure typed API
|
||||
mempalaceAction: (action, args) => {
|
||||
return ipcRenderer.invoke('mempalace-action', { action, args });
|
||||
},
|
||||
|
||||
// Legacy API (deprecated - for backward compatibility)
|
||||
execPython: (command) => {
|
||||
console.warn('[MemPalace] execPython is deprecated. Use mempalaceAction instead.');
|
||||
return ipcRenderer.invoke('exec-python', command);
|
||||
},
|
||||
|
||||
// Utility functions
|
||||
platform: process.platform,
|
||||
versions: process.versions
|
||||
});
|
||||
177
tests/test_secure_mempalace_ipc.js
Normal file
177
tests/test_secure_mempalace_ipc.js
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Tests for secure MemPalace IPC bridge
|
||||
* Issue #1423: [SECURITY] Electron MemPalace bridge allows arbitrary command execution
|
||||
*/
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { setupSecureMemPalaceIPC, executeMemPalaceAction, ALLOWED_ACTIONS } = require('./electron-mempalace-bridge');
|
||||
|
||||
// Mock Electron IPC
|
||||
const mockIpcMain = {
|
||||
handlers: {},
|
||||
handle: function(channel, handler) {
|
||||
this.handlers[channel] = handler;
|
||||
},
|
||||
removeHandler: function(channel) {
|
||||
delete this.handlers[channel];
|
||||
}
|
||||
};
|
||||
|
||||
// Mock child_process.spawn
|
||||
const mockSpawn = jest.fn();
|
||||
|
||||
// Setup before tests
|
||||
test.before(() => {
|
||||
// Mock require
|
||||
const Module = require('module');
|
||||
const originalRequire = Module.prototype.require;
|
||||
|
||||
Module.prototype.require = function(id) {
|
||||
if (id === 'child_process') {
|
||||
return { spawn: mockSpawn };
|
||||
}
|
||||
if (id === 'electron') {
|
||||
return { ipcMain: mockIpcMain };
|
||||
}
|
||||
return originalRequire.apply(this, arguments);
|
||||
};
|
||||
});
|
||||
|
||||
test('ALLOWED_ACTIONS contains expected actions', () => {
|
||||
const expectedActions = ['init', 'mine', 'search', 'status', 'add_drawer'];
|
||||
expectedActions.forEach(action => {
|
||||
assert.ok(ALLOWED_ACTIONS[action], `Should have ${action} action`);
|
||||
assert.ok(ALLOWED_ACTIONS[action].command, `${action} should have command`);
|
||||
assert.ok(ALLOWED_ACTIONS[action].args, `${action} should have args`);
|
||||
assert.ok(ALLOWED_ACTIONS[action].validate, `${action} should have validate function`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Valid init action works', async () => {
|
||||
// Mock spawn to return success
|
||||
const mockChild = {
|
||||
stdout: { on: (event, cb) => { if (event === 'data') cb('OK'); } },
|
||||
stderr: { on: () => {} },
|
||||
on: (event, cb) => { if (event === 'close') cb(0); }
|
||||
};
|
||||
mockSpawn.mockReturnValue(mockChild);
|
||||
|
||||
const result = await executeMemPalaceAction('init', { palacePath: '/safe/path' });
|
||||
assert.equal(result.stdout, 'OK');
|
||||
assert.equal(result.stderr, '');
|
||||
assert.equal(result.code, 0);
|
||||
});
|
||||
|
||||
test('Valid mine action works', async () => {
|
||||
const mockChild = {
|
||||
stdout: { on: (event, cb) => { if (event === 'data') cb('Mined'); } },
|
||||
stderr: { on: () => {} },
|
||||
on: (event, cb) => { if (event === 'close') cb(0); }
|
||||
};
|
||||
mockSpawn.mockReturnValue(mockChild);
|
||||
|
||||
const result = await executeMemPalaceAction('mine', {
|
||||
path: '/safe/path',
|
||||
mode: 'convos',
|
||||
wing: 'test_wing'
|
||||
});
|
||||
assert.equal(result.stdout, 'Mined');
|
||||
});
|
||||
|
||||
test('Rejects unsafe characters in init', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('init', { palacePath: '/path; rm -rf /' }),
|
||||
{ message: /unsafe characters/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rejects unsafe characters in mine', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('mine', {
|
||||
path: '/path; rm -rf /',
|
||||
mode: 'convos',
|
||||
wing: 'test'
|
||||
}),
|
||||
{ message: /unsafe characters/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rejects unsafe characters in search', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('search', {
|
||||
query: 'test; rm -rf /',
|
||||
wing: 'test'
|
||||
}),
|
||||
{ message: /unsafe characters/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rejects unknown actions', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('unknown', {}),
|
||||
{ message: /Unknown action/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rejects invalid mine mode', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('mine', {
|
||||
path: '/safe/path',
|
||||
mode: 'invalid_mode',
|
||||
wing: 'test'
|
||||
}),
|
||||
{ message: /Mode must be one of/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rejects missing required arguments', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('mine', {
|
||||
path: '/safe/path',
|
||||
// Missing mode and wing
|
||||
}),
|
||||
{ message: /must be a string/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Search with optional arguments works', async () => {
|
||||
const mockChild = {
|
||||
stdout: { on: (event, cb) => { if (event === 'data') cb('Results'); } },
|
||||
stderr: { on: () => {} },
|
||||
on: (event, cb) => { if (event === 'close') cb(0); }
|
||||
};
|
||||
mockSpawn.mockReturnValue(mockChild);
|
||||
|
||||
const result = await executeMemPalaceAction('search', {
|
||||
query: 'test query',
|
||||
wing: 'test_wing',
|
||||
room: 'test_room',
|
||||
n: 10
|
||||
});
|
||||
assert.equal(result.stdout, 'Results');
|
||||
});
|
||||
|
||||
test('Rejects unsafe room in search', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('search', {
|
||||
query: 'safe query',
|
||||
wing: 'safe_wing',
|
||||
room: 'room; rm -rf /'
|
||||
}),
|
||||
{ message: /unsafe characters/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rejects unsafe text in add_drawer', async () => {
|
||||
await assert.rejects(
|
||||
() => executeMemPalaceAction('add_drawer', {
|
||||
wing: 'safe_wing',
|
||||
room: 'safe_room',
|
||||
text: 'text; rm -rf /'
|
||||
}),
|
||||
{ message: /unsafe characters/ }
|
||||
);
|
||||
});
|
||||
|
||||
console.log('All secure MemPalace IPC tests passed!');
|
||||
@@ -1,99 +0,0 @@
|
||||
// Test suite for SpatialSearch module
|
||||
// Run: node --test tests/test_spatial_search.js
|
||||
|
||||
const { describe, it, beforeEach } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
|
||||
// Mock DOM for Node.js environment
|
||||
global.document = {
|
||||
createElement: (tag) => ({
|
||||
className: '',
|
||||
style: {},
|
||||
innerHTML: '',
|
||||
textContent: '',
|
||||
addEventListener: () => {},
|
||||
appendChild: () => {},
|
||||
querySelector: () => ({ style: {}, textContent: '' })
|
||||
}),
|
||||
body: { appendChild: () => {} },
|
||||
addEventListener: () => {}
|
||||
};
|
||||
global.window = { camera: null };
|
||||
|
||||
// Load module
|
||||
const SpatialSearch = require('../js/spatial-search.js');
|
||||
|
||||
describe('SpatialSearch', () => {
|
||||
let search;
|
||||
|
||||
beforeEach(() => {
|
||||
search = new SpatialSearch({ maxDistance: 1000 });
|
||||
});
|
||||
|
||||
it('loads correctly', () => {
|
||||
assert.ok(SpatialSearch);
|
||||
});
|
||||
|
||||
it('can be instantiated', () => {
|
||||
assert.ok(search instanceof SpatialSearch);
|
||||
});
|
||||
|
||||
it('can register entities', () => {
|
||||
search.registerEntity('user1', {
|
||||
name: 'Alice',
|
||||
type: 'user',
|
||||
position: { x: 10, y: 0, z: 5 }
|
||||
});
|
||||
const status = search.getStatus();
|
||||
assert.strictEqual(status.entityCount, 1);
|
||||
});
|
||||
|
||||
it('can unregister entities', () => {
|
||||
search.registerEntity('user1', { name: 'Alice', type: 'user', position: { x: 0, y: 0, z: 0 } });
|
||||
search.unregisterEntity('user1');
|
||||
assert.strictEqual(search.getStatus().entityCount, 0);
|
||||
});
|
||||
|
||||
it('can update entity position', () => {
|
||||
search.registerEntity('user1', { name: 'Alice', type: 'user', position: { x: 0, y: 0, z: 0 } });
|
||||
search.updateEntityPosition('user1', { x: 10, y: 0, z: 10 });
|
||||
// Verify by searching with camera position
|
||||
const results = search.searchEntities('alice', { x: 0, y: 0, z: 0 });
|
||||
assert.strictEqual(results.length, 1);
|
||||
assert.ok(results[0].distance > 0);
|
||||
});
|
||||
|
||||
it('calculates distance correctly', () => {
|
||||
const from = { x: 0, y: 0, z: 0 };
|
||||
const to = { x: 3, y: 0, z: 4 };
|
||||
const distance = search._calculateDistance(from, to);
|
||||
assert.strictEqual(distance, 5); // 3-4-5 triangle
|
||||
});
|
||||
|
||||
it('calculates direction correctly', () => {
|
||||
const from = { x: 0, y: 0, z: 0 };
|
||||
assert.strictEqual(search._calculateDirection(from, { x: 0, y: 0, z: 10 }), 'N');
|
||||
assert.strictEqual(search._calculateDirection(from, { x: 10, y: 0, z: 0 }), 'E');
|
||||
assert.strictEqual(search._calculateDirection(from, { x: 0, y: 0, z: -10 }), 'S');
|
||||
assert.strictEqual(search._calculateDirection(from, { x: -10, y: 0, z: 0 }), 'W');
|
||||
});
|
||||
|
||||
it('searches entities correctly', () => {
|
||||
search.registerEntity('1', { name: 'Alice', type: 'user', position: { x: 5, y: 0, z: 0 } });
|
||||
search.registerEntity('2', { name: 'Bob', type: 'user', position: { x: 10, y: 0, z: 0 } });
|
||||
search.registerEntity('3', { name: 'Alice Shop', type: 'object', position: { x: 20, y: 0, z: 0 } });
|
||||
|
||||
const results = search.searchEntities('ali', { x: 0, y: 0, z: 0 });
|
||||
assert.strictEqual(results.length, 2);
|
||||
assert.strictEqual(results[0].name, 'Alice'); // Closer
|
||||
assert.strictEqual(results[1].name, 'Alice Shop');
|
||||
});
|
||||
|
||||
it('gets status', () => {
|
||||
search.registerEntity('1', { name: 'Test', type: 'object', position: { x: 0, y: 0, z: 0 } });
|
||||
const status = search.getStatus();
|
||||
assert.strictEqual(status.entityCount, 1);
|
||||
assert.strictEqual(status.isOpen, false);
|
||||
assert.strictEqual(status.resultCount, 0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user