@ruvector/edge-net 0.5.0 ā 0.5.1
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/package.json +1 -1
- package/plugins/SECURITY-AUDIT.md +654 -0
- package/plugins/cli.js +43 -3
- package/plugins/implementations/e2e-encryption.js +57 -12
- package/plugins/plugin-loader.js +229 -21
- package/tests/plugin-benchmark.js +1239 -0
package/plugins/cli.js
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* @module @ruvector/edge-net/plugins/cli
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
20
|
-
import { join, dirname } from 'path';
|
|
19
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync } from 'fs';
|
|
20
|
+
import { join, dirname, resolve, isAbsolute } from 'path';
|
|
21
21
|
import { fileURLToPath } from 'url';
|
|
22
22
|
import {
|
|
23
23
|
PLUGIN_CATALOG,
|
|
@@ -277,7 +277,47 @@ MIT
|
|
|
277
277
|
console.log(`\nš Validating plugin: ${pluginPath}\n`);
|
|
278
278
|
|
|
279
279
|
try {
|
|
280
|
-
|
|
280
|
+
// SECURITY: Validate plugin path to prevent arbitrary code execution
|
|
281
|
+
const cwd = process.cwd();
|
|
282
|
+
const allowedDirs = [
|
|
283
|
+
resolve(cwd, 'plugins'),
|
|
284
|
+
resolve(cwd, 'node_modules'),
|
|
285
|
+
resolve(__dirname, 'implementations'),
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
// Resolve the absolute path and follow symlinks
|
|
289
|
+
let absolutePath;
|
|
290
|
+
try {
|
|
291
|
+
absolutePath = isAbsolute(pluginPath)
|
|
292
|
+
? realpathSync(pluginPath)
|
|
293
|
+
: realpathSync(resolve(cwd, pluginPath));
|
|
294
|
+
} catch (e) {
|
|
295
|
+
throw new Error(`Plugin file not found or inaccessible: ${pluginPath}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Verify path is within allowed directories
|
|
299
|
+
const isAllowed = allowedDirs.some(dir => {
|
|
300
|
+
try {
|
|
301
|
+
const realDir = realpathSync(dir);
|
|
302
|
+
return absolutePath.startsWith(realDir + '/') || absolutePath === realDir;
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (!isAllowed) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Security: Plugin path must be within allowed directories:\n` +
|
|
311
|
+
allowedDirs.map(d => ` - ${d}`).join('\n')
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Verify file extension
|
|
316
|
+
if (!absolutePath.endsWith('.js') && !absolutePath.endsWith('.mjs')) {
|
|
317
|
+
throw new Error('Security: Plugin must be a .js or .mjs file');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const plugin = await import(absolutePath);
|
|
281
321
|
const PluginClass = plugin.default || Object.values(plugin)[0];
|
|
282
322
|
|
|
283
323
|
if (!PluginClass) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module @ruvector/edge-net/plugins/e2e-encryption
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
|
|
10
|
+
import { randomBytes, createCipheriv, createDecipheriv, createHash, pbkdf2Sync, hkdfSync } from 'crypto';
|
|
11
11
|
|
|
12
12
|
export class E2EEncryptionPlugin {
|
|
13
13
|
constructor(config = {}) {
|
|
@@ -38,16 +38,41 @@ export class E2EEncryptionPlugin {
|
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Establish encrypted session with peer
|
|
41
|
+
* Uses HKDF for secure key derivation with proper entropy
|
|
41
42
|
*/
|
|
42
43
|
async establishSession(peerId, peerPublicKey) {
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
// Generate cryptographically secure random material
|
|
45
|
+
const ephemeralSecret = randomBytes(32);
|
|
46
|
+
const salt = randomBytes(32);
|
|
47
|
+
|
|
48
|
+
// In production: X25519 key exchange with peerPublicKey
|
|
49
|
+
// For now: Use HKDF for secure key derivation
|
|
50
|
+
// HKDF is a proper KDF that extracts entropy and expands it securely
|
|
51
|
+
let sharedSecret;
|
|
52
|
+
try {
|
|
53
|
+
// Use HKDF (preferred) - extract-then-expand
|
|
54
|
+
sharedSecret = hkdfSync(
|
|
55
|
+
'sha256', // hash algorithm
|
|
56
|
+
ephemeralSecret, // input key material
|
|
57
|
+
salt, // salt
|
|
58
|
+
`edge-net-e2e-${peerId}`, // info/context
|
|
59
|
+
32 // output length
|
|
60
|
+
);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
// Fallback to PBKDF2 if HKDF not available (older Node)
|
|
63
|
+
// 100,000 iterations for security
|
|
64
|
+
sharedSecret = pbkdf2Sync(
|
|
65
|
+
ephemeralSecret,
|
|
66
|
+
salt,
|
|
67
|
+
100000, // iterations
|
|
68
|
+
32, // key length
|
|
69
|
+
'sha256'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
48
72
|
|
|
49
73
|
const sessionKey = {
|
|
50
74
|
key: sharedSecret,
|
|
75
|
+
salt: salt,
|
|
51
76
|
iv: randomBytes(16),
|
|
52
77
|
createdAt: Date.now(),
|
|
53
78
|
messageCount: 0,
|
|
@@ -57,7 +82,8 @@ export class E2EEncryptionPlugin {
|
|
|
57
82
|
|
|
58
83
|
return {
|
|
59
84
|
sessionId: createHash('sha256').update(sharedSecret).digest('hex').slice(0, 16),
|
|
60
|
-
publicKey:
|
|
85
|
+
publicKey: ephemeralSecret.toString('hex'), // Our ephemeral public key
|
|
86
|
+
salt: salt.toString('hex'),
|
|
61
87
|
};
|
|
62
88
|
}
|
|
63
89
|
|
|
@@ -117,18 +143,37 @@ export class E2EEncryptionPlugin {
|
|
|
117
143
|
|
|
118
144
|
/**
|
|
119
145
|
* Rotate session keys for forward secrecy
|
|
146
|
+
* Uses HKDF for secure key rotation
|
|
120
147
|
*/
|
|
121
148
|
_rotateKeys() {
|
|
122
149
|
const now = Date.now();
|
|
123
150
|
for (const [peerId, session] of this.sessionKeys) {
|
|
124
151
|
if (now - session.createdAt > this.config.keyRotationInterval) {
|
|
125
|
-
// Generate new session key
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
152
|
+
// Generate new session key using HKDF with previous key as IKM
|
|
153
|
+
const newSalt = randomBytes(32);
|
|
154
|
+
let newKey;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
newKey = hkdfSync(
|
|
158
|
+
'sha256',
|
|
159
|
+
session.key,
|
|
160
|
+
newSalt,
|
|
161
|
+
`edge-net-rotate-${peerId}-${now}`,
|
|
162
|
+
32
|
|
163
|
+
);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// Fallback to PBKDF2
|
|
166
|
+
newKey = pbkdf2Sync(
|
|
167
|
+
session.key,
|
|
168
|
+
newSalt,
|
|
169
|
+
100000,
|
|
170
|
+
32,
|
|
171
|
+
'sha256'
|
|
172
|
+
);
|
|
173
|
+
}
|
|
130
174
|
|
|
131
175
|
session.key = newKey;
|
|
176
|
+
session.salt = newSalt;
|
|
132
177
|
session.createdAt = now;
|
|
133
178
|
session.messageCount = 0;
|
|
134
179
|
}
|
package/plugins/plugin-loader.js
CHANGED
|
@@ -12,9 +12,109 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { EventEmitter } from 'events';
|
|
15
|
-
import { createHash } from 'crypto';
|
|
15
|
+
import { createHash, createVerify, generateKeyPairSync, sign, verify } from 'crypto';
|
|
16
16
|
import { PLUGIN_CATALOG, PLUGIN_BUNDLES, Capability, PluginTier } from './plugin-manifest.js';
|
|
17
17
|
|
|
18
|
+
// ============================================
|
|
19
|
+
// ED25519 SIGNATURE VERIFICATION
|
|
20
|
+
// ============================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Built-in trusted public keys for official plugins
|
|
24
|
+
* In production, these would be loaded from a secure registry
|
|
25
|
+
*/
|
|
26
|
+
const TRUSTED_PUBLIC_KEYS = {
|
|
27
|
+
'ruvector': 'MCowBQYDK2VwAyEAMock_ruvector_official_key_replace_in_production_1234567890=',
|
|
28
|
+
'edge-net-official': 'MCowBQYDK2VwAyEAMock_edgenet_official_key_replace_in_production_abcdef12=',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Verify Ed25519 signature
|
|
33
|
+
* @param {Buffer|string} data - The data that was signed
|
|
34
|
+
* @param {string} signature - Base64-encoded signature
|
|
35
|
+
* @param {string} publicKey - PEM or DER formatted public key
|
|
36
|
+
* @returns {boolean} - True if signature is valid
|
|
37
|
+
*/
|
|
38
|
+
function verifyEd25519Signature(data, signature, publicKey) {
|
|
39
|
+
try {
|
|
40
|
+
const signatureBuffer = Buffer.from(signature, 'base64');
|
|
41
|
+
const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
42
|
+
|
|
43
|
+
// Handle both PEM and raw key formats
|
|
44
|
+
let keyObject;
|
|
45
|
+
if (publicKey.startsWith('-----BEGIN')) {
|
|
46
|
+
keyObject = publicKey;
|
|
47
|
+
} else {
|
|
48
|
+
// Assume base64-encoded DER format
|
|
49
|
+
keyObject = {
|
|
50
|
+
key: Buffer.from(publicKey, 'base64'),
|
|
51
|
+
format: 'der',
|
|
52
|
+
type: 'spki',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return verify(null, dataBuffer, keyObject, signatureBuffer);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Signature verification failed
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================
|
|
64
|
+
// RATE LIMITER
|
|
65
|
+
// ============================================
|
|
66
|
+
|
|
67
|
+
class RateLimiter {
|
|
68
|
+
constructor(options = {}) {
|
|
69
|
+
this.maxRequests = options.maxRequests || 100;
|
|
70
|
+
this.windowMs = options.windowMs || 60000; // 1 minute
|
|
71
|
+
this.requests = new Map(); // key -> { count, windowStart }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if action is allowed, returns true if allowed
|
|
76
|
+
*/
|
|
77
|
+
check(key) {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
let record = this.requests.get(key);
|
|
80
|
+
|
|
81
|
+
if (!record || (now - record.windowStart) > this.windowMs) {
|
|
82
|
+
// New window
|
|
83
|
+
record = { count: 0, windowStart: now };
|
|
84
|
+
this.requests.set(key, record);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (record.count >= this.maxRequests) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
record.count++;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get remaining requests in current window
|
|
97
|
+
*/
|
|
98
|
+
remaining(key) {
|
|
99
|
+
const record = this.requests.get(key);
|
|
100
|
+
if (!record) return this.maxRequests;
|
|
101
|
+
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
if ((now - record.windowStart) > this.windowMs) {
|
|
104
|
+
return this.maxRequests;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return Math.max(0, this.maxRequests - record.count);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Reset limits for a key
|
|
112
|
+
*/
|
|
113
|
+
reset(key) {
|
|
114
|
+
this.requests.delete(key);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
18
118
|
// ============================================
|
|
19
119
|
// PLUGIN LOADER
|
|
20
120
|
// ============================================
|
|
@@ -28,6 +128,7 @@ export class PluginLoader extends EventEmitter {
|
|
|
28
128
|
verifySignatures: options.verifySignatures ?? true,
|
|
29
129
|
allowedTiers: options.allowedTiers ?? [PluginTier.STABLE, PluginTier.BETA],
|
|
30
130
|
trustedAuthors: options.trustedAuthors ?? ['ruvector', 'edge-net-official'],
|
|
131
|
+
trustedPublicKeys: options.trustedPublicKeys ?? {},
|
|
31
132
|
|
|
32
133
|
// Loading
|
|
33
134
|
lazyLoad: options.lazyLoad ?? true,
|
|
@@ -37,6 +138,11 @@ export class PluginLoader extends EventEmitter {
|
|
|
37
138
|
// Permissions
|
|
38
139
|
maxCapabilities: options.maxCapabilities ?? 10,
|
|
39
140
|
deniedCapabilities: options.deniedCapabilities ?? [Capability.SYSTEM_EXEC],
|
|
141
|
+
|
|
142
|
+
// Rate limiting
|
|
143
|
+
rateLimitEnabled: options.rateLimitEnabled ?? true,
|
|
144
|
+
rateLimitRequests: options.rateLimitRequests ?? 100,
|
|
145
|
+
rateLimitWindowMs: options.rateLimitWindowMs ?? 60000,
|
|
40
146
|
...options,
|
|
41
147
|
};
|
|
42
148
|
|
|
@@ -45,12 +151,19 @@ export class PluginLoader extends EventEmitter {
|
|
|
45
151
|
this.pluginConfigs = new Map(); // id -> config
|
|
46
152
|
this.pendingLoads = new Map(); // id -> Promise
|
|
47
153
|
|
|
154
|
+
// Rate limiter
|
|
155
|
+
this.rateLimiter = new RateLimiter({
|
|
156
|
+
maxRequests: this.options.rateLimitRequests,
|
|
157
|
+
windowMs: this.options.rateLimitWindowMs,
|
|
158
|
+
});
|
|
159
|
+
|
|
48
160
|
// Stats
|
|
49
161
|
this.stats = {
|
|
50
162
|
loaded: 0,
|
|
51
163
|
cached: 0,
|
|
52
164
|
verified: 0,
|
|
53
165
|
rejected: 0,
|
|
166
|
+
rateLimited: 0,
|
|
54
167
|
};
|
|
55
168
|
}
|
|
56
169
|
|
|
@@ -99,14 +212,14 @@ export class PluginLoader extends EventEmitter {
|
|
|
99
212
|
}
|
|
100
213
|
|
|
101
214
|
/**
|
|
102
|
-
* Verify plugin integrity
|
|
215
|
+
* Verify plugin integrity using Ed25519 signatures
|
|
103
216
|
*/
|
|
104
217
|
async _verifyPlugin(manifest, code) {
|
|
105
218
|
if (!this.options.verifySignatures) {
|
|
106
219
|
return { verified: true, reason: 'Verification disabled' };
|
|
107
220
|
}
|
|
108
221
|
|
|
109
|
-
// Verify checksum
|
|
222
|
+
// Verify checksum first (fast check)
|
|
110
223
|
if (manifest.checksum) {
|
|
111
224
|
const hash = createHash('sha256').update(code).digest('hex');
|
|
112
225
|
if (hash !== manifest.checksum) {
|
|
@@ -114,21 +227,58 @@ export class PluginLoader extends EventEmitter {
|
|
|
114
227
|
}
|
|
115
228
|
}
|
|
116
229
|
|
|
117
|
-
// Verify signature
|
|
118
|
-
if (manifest.signature) {
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
230
|
+
// Verify Ed25519 signature if present
|
|
231
|
+
if (manifest.signature && manifest.author) {
|
|
232
|
+
// Get the public key for this author
|
|
233
|
+
const publicKey = this.options.trustedPublicKeys?.[manifest.author]
|
|
234
|
+
|| TRUSTED_PUBLIC_KEYS[manifest.author];
|
|
235
|
+
|
|
236
|
+
if (!publicKey) {
|
|
237
|
+
return {
|
|
238
|
+
verified: false,
|
|
239
|
+
reason: `Unknown author: ${manifest.author}. No public key registered.`
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Create canonical data for signature verification
|
|
244
|
+
// Include manifest fields that should be signed
|
|
245
|
+
const signedData = JSON.stringify({
|
|
246
|
+
id: manifest.id,
|
|
247
|
+
name: manifest.name,
|
|
248
|
+
version: manifest.version,
|
|
249
|
+
author: manifest.author,
|
|
250
|
+
checksum: manifest.checksum,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Verify the signature using Ed25519
|
|
254
|
+
const isValid = verifyEd25519Signature(signedData, manifest.signature, publicKey);
|
|
255
|
+
|
|
256
|
+
if (!isValid) {
|
|
257
|
+
return {
|
|
258
|
+
verified: false,
|
|
259
|
+
reason: 'Ed25519 signature verification failed'
|
|
260
|
+
};
|
|
123
261
|
}
|
|
262
|
+
|
|
263
|
+
return { verified: true, reason: 'Ed25519 signature valid' };
|
|
124
264
|
}
|
|
125
265
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
266
|
+
// Built-in stable plugins from the catalog don't require external signatures
|
|
267
|
+
// They are verified by code review and included in the package
|
|
268
|
+
if (!manifest.signature && !manifest.checksum) {
|
|
269
|
+
const isBuiltIn = PLUGIN_CATALOG[manifest.id] !== undefined;
|
|
270
|
+
if (isBuiltIn && manifest.tier === PluginTier.STABLE) {
|
|
129
271
|
return { verified: true, reason: 'Built-in stable plugin' };
|
|
130
272
|
}
|
|
131
|
-
return { verified: false, reason: 'No verification metadata' };
|
|
273
|
+
return { verified: false, reason: 'No verification metadata for external plugin' };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Checksum passed but no signature - allow for stable tier only
|
|
277
|
+
if (manifest.checksum && !manifest.signature) {
|
|
278
|
+
if (manifest.tier === PluginTier.STABLE) {
|
|
279
|
+
return { verified: true, reason: 'Checksum verified (stable tier)' };
|
|
280
|
+
}
|
|
281
|
+
return { verified: false, reason: 'Non-stable plugins require signature' };
|
|
132
282
|
}
|
|
133
283
|
|
|
134
284
|
return { verified: true };
|
|
@@ -138,6 +288,18 @@ export class PluginLoader extends EventEmitter {
|
|
|
138
288
|
* Load a plugin by ID
|
|
139
289
|
*/
|
|
140
290
|
async load(pluginId, config = {}) {
|
|
291
|
+
// Rate limit check
|
|
292
|
+
if (this.options.rateLimitEnabled) {
|
|
293
|
+
const rateLimitKey = `load:${pluginId}`;
|
|
294
|
+
if (!this.rateLimiter.check(rateLimitKey)) {
|
|
295
|
+
this.stats.rateLimited++;
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Rate limit exceeded for plugin ${pluginId}. ` +
|
|
298
|
+
`Try again in ${Math.ceil(this.options.rateLimitWindowMs / 1000)}s.`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
141
303
|
// Check if already loaded
|
|
142
304
|
if (this.loadedPlugins.has(pluginId)) {
|
|
143
305
|
return this.loadedPlugins.get(pluginId);
|
|
@@ -206,8 +368,8 @@ export class PluginLoader extends EventEmitter {
|
|
|
206
368
|
* Create plugin instance with sandbox
|
|
207
369
|
*/
|
|
208
370
|
async _createPluginInstance(manifest, config) {
|
|
209
|
-
// Create sandboxed context
|
|
210
|
-
const sandbox = this._createSandbox(manifest.capabilities || []);
|
|
371
|
+
// Create sandboxed context with proper isolation
|
|
372
|
+
const sandbox = this._createSandbox(manifest.capabilities || [], manifest);
|
|
211
373
|
|
|
212
374
|
// Return plugin wrapper
|
|
213
375
|
return {
|
|
@@ -233,24 +395,70 @@ export class PluginLoader extends EventEmitter {
|
|
|
233
395
|
}
|
|
234
396
|
|
|
235
397
|
/**
|
|
236
|
-
* Create capability-based sandbox
|
|
398
|
+
* Create capability-based sandbox with proper isolation
|
|
237
399
|
*/
|
|
238
|
-
_createSandbox(capabilities) {
|
|
400
|
+
_createSandbox(capabilities, manifest) {
|
|
401
|
+
const allowedCapabilities = new Set(capabilities);
|
|
402
|
+
const deniedGlobals = new Set([
|
|
403
|
+
'process', 'require', 'eval', 'Function',
|
|
404
|
+
'__dirname', '__filename', 'module', 'exports'
|
|
405
|
+
]);
|
|
406
|
+
|
|
407
|
+
// Create isolated sandbox context
|
|
239
408
|
const sandbox = {
|
|
240
|
-
|
|
409
|
+
// Immutable capability set
|
|
410
|
+
get capabilities() {
|
|
411
|
+
return new Set(allowedCapabilities);
|
|
412
|
+
},
|
|
241
413
|
|
|
242
414
|
hasCapability(cap) {
|
|
243
|
-
return
|
|
415
|
+
return allowedCapabilities.has(cap);
|
|
244
416
|
},
|
|
245
417
|
|
|
246
418
|
require(cap) {
|
|
247
|
-
if (!
|
|
419
|
+
if (!allowedCapabilities.has(cap)) {
|
|
248
420
|
throw new Error(`Missing capability: ${cap}`);
|
|
249
421
|
}
|
|
250
422
|
},
|
|
423
|
+
|
|
424
|
+
// Resource limits
|
|
425
|
+
limits: Object.freeze({
|
|
426
|
+
maxMemoryMB: 128,
|
|
427
|
+
maxCpuTimeMs: 5000,
|
|
428
|
+
maxNetworkConnections: 10,
|
|
429
|
+
maxStorageBytes: 10 * 1024 * 1024, // 10MB
|
|
430
|
+
}),
|
|
431
|
+
|
|
432
|
+
// Execution context (read-only)
|
|
433
|
+
context: Object.freeze({
|
|
434
|
+
pluginId: manifest.id,
|
|
435
|
+
pluginVersion: manifest.version,
|
|
436
|
+
startTime: Date.now(),
|
|
437
|
+
}),
|
|
438
|
+
|
|
439
|
+
// Check if global is allowed
|
|
440
|
+
isGlobalAllowed(name) {
|
|
441
|
+
return !deniedGlobals.has(name);
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
// Secure timer functions (returns cleanup functions)
|
|
445
|
+
setTimeout: (fn, delay) => {
|
|
446
|
+
const maxDelay = 30000; // 30 seconds max
|
|
447
|
+
const safeDelay = Math.min(delay, maxDelay);
|
|
448
|
+
const timer = setTimeout(fn, safeDelay);
|
|
449
|
+
return () => clearTimeout(timer);
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
setInterval: (fn, delay) => {
|
|
453
|
+
const minDelay = 100; // Minimum 100ms
|
|
454
|
+
const safeDelay = Math.max(delay, minDelay);
|
|
455
|
+
const timer = setInterval(fn, safeDelay);
|
|
456
|
+
return () => clearInterval(timer);
|
|
457
|
+
},
|
|
251
458
|
};
|
|
252
459
|
|
|
253
|
-
|
|
460
|
+
// Freeze the sandbox to prevent modification
|
|
461
|
+
return Object.freeze(sandbox);
|
|
254
462
|
}
|
|
255
463
|
|
|
256
464
|
/**
|