@papicandela/mcx-core 0.2.1 → 0.2.5

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.
@@ -75,83 +75,139 @@ export function generateNetworkIsolationCode(policy: NetworkPolicy): string {
75
75
  }
76
76
 
77
77
  if (policy.mode === "blocked") {
78
+ // SECURITY: Wrap in IIFE to prevent user code from accessing __original_fetch
79
+ // Use Object.defineProperty with writable:false to prevent user code from overwriting
78
80
  return `
79
81
  // Network isolation: BLOCKED
80
- const __original_fetch = globalThis.fetch;
81
- globalThis.fetch = async function(url, options) {
82
- throw new Error('Network access is blocked in sandbox. Use adapters instead.');
83
- };
82
+ (function() {
83
+ const blockedFetch = async function() {
84
+ throw new Error('Network access is blocked in sandbox. Use adapters instead.');
85
+ };
86
+ Object.defineProperty(globalThis, 'fetch', {
87
+ value: blockedFetch,
88
+ writable: false,
89
+ configurable: false
90
+ });
91
+ })();
84
92
 
85
93
  // Block XMLHttpRequest
86
- globalThis.XMLHttpRequest = class {
87
- constructor() {
88
- throw new Error('XMLHttpRequest is blocked in sandbox.');
89
- }
90
- };
94
+ Object.defineProperty(globalThis, 'XMLHttpRequest', {
95
+ value: class {
96
+ constructor() {
97
+ throw new Error('XMLHttpRequest is blocked in sandbox.');
98
+ }
99
+ },
100
+ writable: false,
101
+ configurable: false
102
+ });
91
103
 
92
104
  // Block WebSocket
93
- globalThis.WebSocket = class {
94
- constructor(url) {
95
- throw new Error('WebSocket is blocked in sandbox.');
96
- }
97
- };
105
+ Object.defineProperty(globalThis, 'WebSocket', {
106
+ value: class {
107
+ constructor() {
108
+ throw new Error('WebSocket is blocked in sandbox.');
109
+ }
110
+ },
111
+ writable: false,
112
+ configurable: false
113
+ });
98
114
 
99
115
  // Block EventSource (SSE)
100
- globalThis.EventSource = class {
101
- constructor(url) {
102
- throw new Error('EventSource is blocked in sandbox.');
103
- }
104
- };
116
+ Object.defineProperty(globalThis, 'EventSource', {
117
+ value: class {
118
+ constructor() {
119
+ throw new Error('EventSource is blocked in sandbox.');
120
+ }
121
+ },
122
+ writable: false,
123
+ configurable: false
124
+ });
105
125
  `;
106
126
  }
107
127
 
108
128
  // mode === 'allowed' - whitelist specific domains
129
+ // SECURITY: Wrap in IIFE to prevent user code from accessing internals
130
+ // Use Object.defineProperty with writable:false to prevent user code from overwriting
109
131
  const domainsJson = JSON.stringify(policy.domains);
110
132
  return `
111
133
  // Network isolation: ALLOWED (whitelist)
112
- const __allowed_domains = ${domainsJson};
113
- const __original_fetch = globalThis.fetch;
134
+ (function() {
135
+ const _domains = ${domainsJson};
136
+ const _real_fetch = globalThis.fetch;
114
137
 
115
- function __isUrlAllowed(url) {
116
- try {
117
- const hostname = new URL(url).hostname;
118
- return __allowed_domains.some(d => hostname === d || hostname.endsWith('.' + d));
119
- } catch {
120
- return false;
138
+ // Block private/link-local IPs to prevent DNS rebinding attacks
139
+ function _isPrivateIp(hostname) {
140
+ return /^(localhost|127\\.|10\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.|169\\.254\\.|\\[::1\\]|\\[fc|\\[fd)/.test(hostname);
121
141
  }
122
- }
123
142
 
124
- globalThis.fetch = async function(url, options) {
125
- const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url;
126
- if (!__isUrlAllowed(urlStr)) {
127
- const hostname = new URL(urlStr).hostname;
128
- throw new Error(\`Network access blocked: \${hostname} not in allowed domains: \${__allowed_domains.join(', ')}\`);
143
+ function _isUrlAllowed(url) {
144
+ try {
145
+ const parsed = new URL(url);
146
+ // Only allow http/https protocols
147
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return false;
148
+ const hostname = parsed.hostname;
149
+ if (!hostname || _isPrivateIp(hostname)) return false;
150
+ return _domains.some(d => d && (hostname === d || hostname.endsWith('.' + d)));
151
+ } catch {
152
+ return false;
153
+ }
129
154
  }
130
- return __original_fetch(url, options);
131
- };
132
155
 
133
- // Block XMLHttpRequest (not easily whitelistable)
134
- globalThis.XMLHttpRequest = class {
135
- constructor() {
136
- throw new Error('XMLHttpRequest is blocked. Use fetch() with allowed domains.');
137
- }
138
- };
156
+ const whitelistedFetch = async function(url, options) {
157
+ // Safely extract URL string from various input types
158
+ let urlStr;
159
+ try {
160
+ if (typeof url === 'string') urlStr = url;
161
+ else if (url instanceof URL) urlStr = url.toString();
162
+ else if (url && typeof url.url === 'string') urlStr = url.url;
163
+ else throw new Error('Invalid URL type');
164
+ } catch {
165
+ throw new Error('Network access blocked: could not determine request URL.');
166
+ }
167
+ if (!_isUrlAllowed(urlStr)) {
168
+ throw new Error('Network access blocked: domain not in allowed list.');
169
+ }
170
+ return _real_fetch(url, options);
171
+ };
172
+
173
+ Object.defineProperty(globalThis, 'fetch', {
174
+ value: whitelistedFetch,
175
+ writable: false,
176
+ configurable: false
177
+ });
178
+ })();
139
179
 
140
- // Block WebSocket (would need separate whitelist)
141
- globalThis.WebSocket = class {
142
- constructor(url) {
143
- if (!__isUrlAllowed(url)) {
144
- throw new Error('WebSocket blocked: domain not in allowed list.');
180
+ // Block XMLHttpRequest (not easily whitelistable)
181
+ Object.defineProperty(globalThis, 'XMLHttpRequest', {
182
+ value: class {
183
+ constructor() {
184
+ throw new Error('XMLHttpRequest is blocked. Use fetch() with allowed domains.');
145
185
  }
146
- throw new Error('WebSocket not supported in sandbox even for allowed domains.');
147
- }
148
- };
186
+ },
187
+ writable: false,
188
+ configurable: false
189
+ });
190
+
191
+ // Block WebSocket - opaque error to prevent allowlist enumeration
192
+ Object.defineProperty(globalThis, 'WebSocket', {
193
+ value: class {
194
+ constructor() {
195
+ throw new Error('WebSocket is not supported in sandbox.');
196
+ }
197
+ },
198
+ writable: false,
199
+ configurable: false
200
+ });
149
201
 
150
202
  // Block EventSource
151
- globalThis.EventSource = class {
152
- constructor(url) {
153
- throw new Error('EventSource is blocked in sandbox.');
154
- }
155
- };
203
+ Object.defineProperty(globalThis, 'EventSource', {
204
+ value: class {
205
+ constructor() {
206
+ throw new Error('EventSource is blocked in sandbox.');
207
+ }
208
+ },
209
+ writable: false,
210
+ configurable: false
211
+ });
156
212
  `;
157
213
  }
@@ -48,7 +48,8 @@ export function generateTypes(
48
48
  // Generate input interface for each tool with parameters
49
49
  for (const [toolName, tool] of Object.entries(adapter.tools)) {
50
50
  if (tool.parameters && Object.keys(tool.parameters).length > 0) {
51
- const inputTypeName = `${capitalize(safeName)}_${capitalize(toolName)}_Input`;
51
+ const safeToolNameForType = sanitizeIdentifier(toolName);
52
+ const inputTypeName = `${capitalize(safeName)}_${capitalize(safeToolNameForType)}_Input`;
52
53
  lines.push(generateInputInterface(inputTypeName, tool.parameters, includeDescriptions));
53
54
  lines.push("");
54
55
  }
@@ -56,17 +57,18 @@ export function generateTypes(
56
57
 
57
58
  // Generate adapter declaration
58
59
  if (includeDescriptions && adapter.description) {
59
- lines.push(`/** ${adapter.description} */`);
60
+ lines.push(`/** ${sanitizeJSDoc(adapter.description)} */`);
60
61
  }
61
62
  lines.push(`declare const ${safeName}: {`);
62
63
 
63
64
  for (const [toolName, tool] of Object.entries(adapter.tools)) {
64
65
  const safeToolName = sanitizeIdentifier(toolName);
65
66
  const hasParams = tool.parameters && Object.keys(tool.parameters).length > 0;
66
- const inputTypeName = `${capitalize(safeName)}_${capitalize(toolName)}_Input`;
67
+ // Use sanitized tool name in type name to ensure consistency
68
+ const inputTypeName = `${capitalize(safeName)}_${capitalize(safeToolName)}_Input`;
67
69
 
68
70
  if (includeDescriptions && tool.description) {
69
- lines.push(` /** ${tool.description} */`);
71
+ lines.push(` /** ${sanitizeJSDoc(tool.description)} */`);
70
72
  }
71
73
 
72
74
  const paramStr = hasParams ? `params: ${inputTypeName}` : "";
@@ -83,7 +85,8 @@ export function generateTypes(
83
85
 
84
86
  /**
85
87
  * Generate a compact type summary for token-constrained contexts.
86
- * Returns a condensed one-liner per adapter.
88
+ * Only shows adapter names and method count to minimize context usage.
89
+ * Use mcx_search to discover specific methods.
87
90
  *
88
91
  * @param adapters - Array of adapters
89
92
  * @returns Compact summary string
@@ -91,8 +94,8 @@ export function generateTypes(
91
94
  export function generateTypesSummary(adapters: Adapter[]): string {
92
95
  return adapters
93
96
  .map((adapter) => {
94
- const methods = Object.keys(adapter.tools).join(", ");
95
- return `${adapter.name}: { ${methods} }`;
97
+ const count = Object.keys(adapter.tools).length;
98
+ return `- ${adapter.name} (${count} methods)`;
96
99
  })
97
100
  .join("\n");
98
101
  }
@@ -108,13 +111,16 @@ function generateInputInterface(
108
111
  const lines: string[] = [`interface ${typeName} {`];
109
112
 
110
113
  for (const [paramName, param] of Object.entries(parameters)) {
114
+ // Sanitize parameter name to prevent injection
115
+ const safeParamName = sanitizeIdentifier(paramName);
116
+
111
117
  if (includeDescriptions && param.description) {
112
- lines.push(` /** ${param.description} */`);
118
+ lines.push(` /** ${sanitizeJSDoc(param.description)} */`);
113
119
  }
114
120
 
115
121
  const tsType = paramTypeToTS(param.type);
116
122
  const optional = param.required === false ? "?" : "";
117
- lines.push(` ${paramName}${optional}: ${tsType};`);
123
+ lines.push(` ${safeParamName}${optional}: ${tsType};`);
118
124
  }
119
125
 
120
126
  lines.push("}");
@@ -170,3 +176,11 @@ export function sanitizeIdentifier(name: string): string {
170
176
  function capitalize(str: string): string {
171
177
  return str.charAt(0).toUpperCase() + str.slice(1);
172
178
  }
179
+
180
+ /**
181
+ * Sanitize text for use in JSDoc comments.
182
+ * Prevents comment injection via `*​/` sequences.
183
+ */
184
+ function sanitizeJSDoc(text: string): string {
185
+ return text.replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ");
186
+ }