@objectstack/core 2.0.1 → 2.0.3

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.d.cts CHANGED
@@ -1236,6 +1236,7 @@ declare class PluginPermissionEnforcer {
1236
1236
  private checkPermission;
1237
1237
  private checkServiceAccess;
1238
1238
  private checkHookAccess;
1239
+ private matchGlob;
1239
1240
  private checkFileRead;
1240
1241
  private checkFileWrite;
1241
1242
  private checkNetworkAccess;
@@ -1395,9 +1396,12 @@ interface SandboxContext {
1395
1396
  * and access controls
1396
1397
  */
1397
1398
  declare class PluginSandboxRuntime {
1399
+ private static readonly MONITORING_INTERVAL_MS;
1398
1400
  private logger;
1399
1401
  private sandboxes;
1400
1402
  private monitoringIntervals;
1403
+ private memoryBaselines;
1404
+ private cpuBaselines;
1401
1405
  constructor(logger: ObjectLogger);
1402
1406
  /**
1403
1407
  * Create a sandbox for a plugin
@@ -1416,14 +1420,12 @@ declare class PluginSandboxRuntime {
1416
1420
  };
1417
1421
  /**
1418
1422
  * Check file system access
1419
- * WARNING: Uses simple prefix matching. For production, use proper path
1420
- * resolution with path.resolve() and path.normalize() to prevent traversal.
1423
+ * Uses path.resolve() and path.normalize() to prevent directory traversal.
1421
1424
  */
1422
1425
  private checkFileAccess;
1423
1426
  /**
1424
1427
  * Check network access
1425
- * WARNING: Uses simple string matching. For production, use proper URL
1426
- * parsing with new URL() and check hostname property.
1428
+ * Uses URL parsing to properly validate hostnames.
1427
1429
  */
1428
1430
  private checkNetworkAccess;
1429
1431
  /**
@@ -1456,10 +1458,9 @@ declare class PluginSandboxRuntime {
1456
1458
  /**
1457
1459
  * Update resource usage statistics
1458
1460
  *
1459
- * NOTE: Currently uses global process.memoryUsage() which tracks the entire
1460
- * Node.js process, not individual plugins. For production, implement proper
1461
- * per-plugin tracking using V8 heap snapshots or allocation tracking at
1462
- * plugin boundaries.
1461
+ * Tracks per-plugin memory and CPU usage using delta from baseline
1462
+ * captured at sandbox creation time. This is an approximation since
1463
+ * true per-plugin isolation isn't possible in a single Node.js process.
1463
1464
  */
1464
1465
  private updateResourceUsage;
1465
1466
  /**
@@ -1667,9 +1668,7 @@ declare class PluginStateManager {
1667
1668
  */
1668
1669
  clearState(pluginId: string): void;
1669
1670
  /**
1670
- * Calculate simple checksum for state verification
1671
- * WARNING: This is a simple hash for demo purposes.
1672
- * In production, use a cryptographic hash like SHA-256.
1671
+ * Calculate checksum for state verification using SHA-256.
1673
1672
  */
1674
1673
  private calculateChecksum;
1675
1674
  /**
package/dist/index.d.ts CHANGED
@@ -1236,6 +1236,7 @@ declare class PluginPermissionEnforcer {
1236
1236
  private checkPermission;
1237
1237
  private checkServiceAccess;
1238
1238
  private checkHookAccess;
1239
+ private matchGlob;
1239
1240
  private checkFileRead;
1240
1241
  private checkFileWrite;
1241
1242
  private checkNetworkAccess;
@@ -1395,9 +1396,12 @@ interface SandboxContext {
1395
1396
  * and access controls
1396
1397
  */
1397
1398
  declare class PluginSandboxRuntime {
1399
+ private static readonly MONITORING_INTERVAL_MS;
1398
1400
  private logger;
1399
1401
  private sandboxes;
1400
1402
  private monitoringIntervals;
1403
+ private memoryBaselines;
1404
+ private cpuBaselines;
1401
1405
  constructor(logger: ObjectLogger);
1402
1406
  /**
1403
1407
  * Create a sandbox for a plugin
@@ -1416,14 +1420,12 @@ declare class PluginSandboxRuntime {
1416
1420
  };
1417
1421
  /**
1418
1422
  * Check file system access
1419
- * WARNING: Uses simple prefix matching. For production, use proper path
1420
- * resolution with path.resolve() and path.normalize() to prevent traversal.
1423
+ * Uses path.resolve() and path.normalize() to prevent directory traversal.
1421
1424
  */
1422
1425
  private checkFileAccess;
1423
1426
  /**
1424
1427
  * Check network access
1425
- * WARNING: Uses simple string matching. For production, use proper URL
1426
- * parsing with new URL() and check hostname property.
1428
+ * Uses URL parsing to properly validate hostnames.
1427
1429
  */
1428
1430
  private checkNetworkAccess;
1429
1431
  /**
@@ -1456,10 +1458,9 @@ declare class PluginSandboxRuntime {
1456
1458
  /**
1457
1459
  * Update resource usage statistics
1458
1460
  *
1459
- * NOTE: Currently uses global process.memoryUsage() which tracks the entire
1460
- * Node.js process, not individual plugins. For production, implement proper
1461
- * per-plugin tracking using V8 heap snapshots or allocation tracking at
1462
- * plugin boundaries.
1461
+ * Tracks per-plugin memory and CPU usage using delta from baseline
1462
+ * captured at sandbox creation time. This is an approximation since
1463
+ * true per-plugin isolation isn't possible in a single Node.js process.
1463
1464
  */
1464
1465
  private updateResourceUsage;
1465
1466
  /**
@@ -1667,9 +1668,7 @@ declare class PluginStateManager {
1667
1668
  */
1668
1669
  clearState(pluginId: string): void;
1669
1670
  /**
1670
- * Calculate simple checksum for state verification
1671
- * WARNING: This is a simple hash for demo purposes.
1672
- * In production, use a cryptographic hash like SHA-256.
1671
+ * Calculate checksum for state verification using SHA-256.
1673
1672
  */
1674
1673
  private calculateChecksum;
1675
1674
  /**
package/dist/index.js CHANGED
@@ -2081,8 +2081,18 @@ var TestRunner = class {
2081
2081
  }
2082
2082
  return result;
2083
2083
  }
2084
- resolveVariables(action, _context) {
2085
- return action;
2084
+ resolveVariables(action, context) {
2085
+ const actionStr = JSON.stringify(action);
2086
+ const resolved = actionStr.replace(/\{\{([^}]+)\}\}/g, (_match, varPath) => {
2087
+ const value = this.getValueByPath(context, varPath.trim());
2088
+ if (value === void 0) return _match;
2089
+ return typeof value === "string" ? value : JSON.stringify(value);
2090
+ });
2091
+ try {
2092
+ return JSON.parse(resolved);
2093
+ } catch {
2094
+ return action;
2095
+ }
2086
2096
  }
2087
2097
  getValueByPath(obj, path) {
2088
2098
  if (!path) return obj;
@@ -2419,9 +2429,38 @@ var PluginSignatureVerifier = class {
2419
2429
  return false;
2420
2430
  }
2421
2431
  }
2422
- async verifyCryptoSignatureBrowser(_data, _signature, _publicKey) {
2423
- this.logger.warn("Browser signature verification not yet implemented");
2424
- return false;
2432
+ async verifyCryptoSignatureBrowser(data, signature, publicKey) {
2433
+ try {
2434
+ const subtle = globalThis.crypto?.subtle;
2435
+ if (!subtle) {
2436
+ this.logger.error("SubtleCrypto not available in this environment");
2437
+ return false;
2438
+ }
2439
+ const pemBody = publicKey.replace(/-----BEGIN PUBLIC KEY-----/, "").replace(/-----END PUBLIC KEY-----/, "").replace(/\s/g, "");
2440
+ const keyBytes = Uint8Array.from(atob(pemBody), (c) => c.charCodeAt(0));
2441
+ let importAlgorithm;
2442
+ let verifyAlgorithm;
2443
+ if (this.config.algorithm === "ES256") {
2444
+ importAlgorithm = { name: "ECDSA", namedCurve: "P-256" };
2445
+ verifyAlgorithm = { name: "ECDSA", hash: "SHA-256" };
2446
+ } else {
2447
+ importAlgorithm = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
2448
+ verifyAlgorithm = { name: "RSASSA-PKCS1-v1_5" };
2449
+ }
2450
+ const cryptoKey = await subtle.importKey(
2451
+ "spki",
2452
+ keyBytes,
2453
+ importAlgorithm,
2454
+ false,
2455
+ ["verify"]
2456
+ );
2457
+ const signatureBytes = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0));
2458
+ const dataBytes = new TextEncoder().encode(data);
2459
+ return await subtle.verify(verifyAlgorithm, cryptoKey, signatureBytes, dataBytes);
2460
+ } catch (error) {
2461
+ this.logger.error("Browser signature verification failed", error);
2462
+ return false;
2463
+ }
2425
2464
  }
2426
2465
  validateConfig() {
2427
2466
  if (!this.config.trustedPublicKeys || this.config.trustedPublicKeys.size === 0) {
@@ -2639,29 +2678,48 @@ var PluginPermissionEnforcer = class {
2639
2678
  return false;
2640
2679
  });
2641
2680
  }
2642
- checkFileRead(capabilities, _path) {
2681
+ matchGlob(pattern, str) {
2682
+ const regexStr = pattern.split("**").map((segment) => {
2683
+ const escaped = segment.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
2684
+ return escaped.replace(/\*/g, "[^/]*");
2685
+ }).join(".*");
2686
+ return new RegExp(`^${regexStr}$`).test(str);
2687
+ }
2688
+ checkFileRead(capabilities, path) {
2643
2689
  return capabilities.some((cap) => {
2644
2690
  const protocolId = cap.protocol.id;
2645
2691
  if (protocolId.includes("protocol.filesystem.read")) {
2646
- return true;
2692
+ const paths = cap.metadata?.paths;
2693
+ if (!Array.isArray(paths) || paths.length === 0) {
2694
+ return true;
2695
+ }
2696
+ return paths.some((p) => typeof p === "string" && this.matchGlob(p, path));
2647
2697
  }
2648
2698
  return false;
2649
2699
  });
2650
2700
  }
2651
- checkFileWrite(capabilities, _path) {
2701
+ checkFileWrite(capabilities, path) {
2652
2702
  return capabilities.some((cap) => {
2653
2703
  const protocolId = cap.protocol.id;
2654
2704
  if (protocolId.includes("protocol.filesystem.write")) {
2655
- return true;
2705
+ const paths = cap.metadata?.paths;
2706
+ if (!Array.isArray(paths) || paths.length === 0) {
2707
+ return true;
2708
+ }
2709
+ return paths.some((p) => typeof p === "string" && this.matchGlob(p, path));
2656
2710
  }
2657
2711
  return false;
2658
2712
  });
2659
2713
  }
2660
- checkNetworkAccess(capabilities, _url) {
2714
+ checkNetworkAccess(capabilities, url) {
2661
2715
  return capabilities.some((cap) => {
2662
2716
  const protocolId = cap.protocol.id;
2663
2717
  if (protocolId.includes("protocol.network")) {
2664
- return true;
2718
+ const hosts = cap.metadata?.hosts;
2719
+ if (!Array.isArray(hosts) || hosts.length === 0) {
2720
+ return true;
2721
+ }
2722
+ return hosts.some((h) => typeof h === "string" && this.matchGlob(h, url));
2665
2723
  }
2666
2724
  return false;
2667
2725
  });
@@ -2928,12 +2986,16 @@ var PluginPermissionManager = class {
2928
2986
  };
2929
2987
 
2930
2988
  // src/security/sandbox-runtime.ts
2931
- var PluginSandboxRuntime = class {
2989
+ import nodePath from "path";
2990
+ var _PluginSandboxRuntime = class _PluginSandboxRuntime {
2932
2991
  constructor(logger) {
2933
2992
  // Active sandboxes (pluginId -> context)
2934
2993
  this.sandboxes = /* @__PURE__ */ new Map();
2935
2994
  // Resource monitoring intervals
2936
2995
  this.monitoringIntervals = /* @__PURE__ */ new Map();
2996
+ // Per-plugin resource baselines for delta tracking
2997
+ this.memoryBaselines = /* @__PURE__ */ new Map();
2998
+ this.cpuBaselines = /* @__PURE__ */ new Map();
2937
2999
  this.logger = logger.child({ component: "SandboxRuntime" });
2938
3000
  }
2939
3001
  /**
@@ -2954,6 +3016,9 @@ var PluginSandboxRuntime = class {
2954
3016
  }
2955
3017
  };
2956
3018
  this.sandboxes.set(pluginId, context);
3019
+ const baselineMemory = getMemoryUsage();
3020
+ this.memoryBaselines.set(pluginId, baselineMemory.heapUsed);
3021
+ this.cpuBaselines.set(pluginId, process.cpuUsage());
2957
3022
  this.startResourceMonitoring(pluginId);
2958
3023
  this.logger.info("Sandbox created", {
2959
3024
  pluginId,
@@ -2972,6 +3037,8 @@ var PluginSandboxRuntime = class {
2972
3037
  return;
2973
3038
  }
2974
3039
  this.stopResourceMonitoring(pluginId);
3040
+ this.memoryBaselines.delete(pluginId);
3041
+ this.cpuBaselines.delete(pluginId);
2975
3042
  this.sandboxes.delete(pluginId);
2976
3043
  this.logger.info("Sandbox destroyed", { pluginId });
2977
3044
  }
@@ -2999,45 +3066,46 @@ var PluginSandboxRuntime = class {
2999
3066
  }
3000
3067
  /**
3001
3068
  * Check file system access
3002
- * WARNING: Uses simple prefix matching. For production, use proper path
3003
- * resolution with path.resolve() and path.normalize() to prevent traversal.
3069
+ * Uses path.resolve() and path.normalize() to prevent directory traversal.
3004
3070
  */
3005
- checkFileAccess(config, path) {
3071
+ checkFileAccess(config, filePath) {
3006
3072
  if (config.level === "none") {
3007
3073
  return { allowed: true };
3008
3074
  }
3009
3075
  if (!config.filesystem) {
3010
3076
  return { allowed: false, reason: "File system access not configured" };
3011
3077
  }
3012
- if (!path) {
3078
+ if (!filePath) {
3013
3079
  return { allowed: config.filesystem.mode !== "none" };
3014
3080
  }
3015
3081
  const allowedPaths = config.filesystem.allowedPaths || [];
3082
+ const resolvedPath = nodePath.normalize(nodePath.resolve(filePath));
3016
3083
  const isAllowed = allowedPaths.some((allowed) => {
3017
- return path.startsWith(allowed);
3084
+ const resolvedAllowed = nodePath.normalize(nodePath.resolve(allowed));
3085
+ return resolvedPath.startsWith(resolvedAllowed);
3018
3086
  });
3019
3087
  if (allowedPaths.length > 0 && !isAllowed) {
3020
3088
  return {
3021
3089
  allowed: false,
3022
- reason: `Path not in allowed list: ${path}`
3090
+ reason: `Path not in allowed list: ${filePath}`
3023
3091
  };
3024
3092
  }
3025
3093
  const deniedPaths = config.filesystem.deniedPaths || [];
3026
3094
  const isDenied = deniedPaths.some((denied) => {
3027
- return path.startsWith(denied);
3095
+ const resolvedDenied = nodePath.normalize(nodePath.resolve(denied));
3096
+ return resolvedPath.startsWith(resolvedDenied);
3028
3097
  });
3029
3098
  if (isDenied) {
3030
3099
  return {
3031
3100
  allowed: false,
3032
- reason: `Path is explicitly denied: ${path}`
3101
+ reason: `Path is explicitly denied: ${filePath}`
3033
3102
  };
3034
3103
  }
3035
3104
  return { allowed: true };
3036
3105
  }
3037
3106
  /**
3038
3107
  * Check network access
3039
- * WARNING: Uses simple string matching. For production, use proper URL
3040
- * parsing with new URL() and check hostname property.
3108
+ * Uses URL parsing to properly validate hostnames.
3041
3109
  */
3042
3110
  checkNetworkAccess(config, url) {
3043
3111
  if (config.level === "none") {
@@ -3052,10 +3120,16 @@ var PluginSandboxRuntime = class {
3052
3120
  if (!url) {
3053
3121
  return { allowed: config.network.mode !== "none" };
3054
3122
  }
3123
+ let parsedHostname;
3124
+ try {
3125
+ parsedHostname = new URL(url).hostname;
3126
+ } catch {
3127
+ return { allowed: false, reason: `Invalid URL: ${url}` };
3128
+ }
3055
3129
  const allowedHosts = config.network.allowedHosts || [];
3056
3130
  if (allowedHosts.length > 0) {
3057
3131
  const isAllowed = allowedHosts.some((host) => {
3058
- return url.includes(host);
3132
+ return parsedHostname === host;
3059
3133
  });
3060
3134
  if (!isAllowed) {
3061
3135
  return {
@@ -3066,7 +3140,7 @@ var PluginSandboxRuntime = class {
3066
3140
  }
3067
3141
  const deniedHosts = config.network.deniedHosts || [];
3068
3142
  const isDenied = deniedHosts.some((host) => {
3069
- return url.includes(host);
3143
+ return parsedHostname === host;
3070
3144
  });
3071
3145
  if (isDenied) {
3072
3146
  return {
@@ -3143,7 +3217,7 @@ var PluginSandboxRuntime = class {
3143
3217
  startResourceMonitoring(pluginId) {
3144
3218
  const interval = setInterval(() => {
3145
3219
  this.updateResourceUsage(pluginId);
3146
- }, 5e3);
3220
+ }, _PluginSandboxRuntime.MONITORING_INTERVAL_MS);
3147
3221
  this.monitoringIntervals.set(pluginId, interval);
3148
3222
  }
3149
3223
  /**
@@ -3159,10 +3233,9 @@ var PluginSandboxRuntime = class {
3159
3233
  /**
3160
3234
  * Update resource usage statistics
3161
3235
  *
3162
- * NOTE: Currently uses global process.memoryUsage() which tracks the entire
3163
- * Node.js process, not individual plugins. For production, implement proper
3164
- * per-plugin tracking using V8 heap snapshots or allocation tracking at
3165
- * plugin boundaries.
3236
+ * Tracks per-plugin memory and CPU usage using delta from baseline
3237
+ * captured at sandbox creation time. This is an approximation since
3238
+ * true per-plugin isolation isn't possible in a single Node.js process.
3166
3239
  */
3167
3240
  updateResourceUsage(pluginId) {
3168
3241
  const context = this.sandboxes.get(pluginId);
@@ -3170,12 +3243,21 @@ var PluginSandboxRuntime = class {
3170
3243
  return;
3171
3244
  }
3172
3245
  const memoryUsage = getMemoryUsage();
3173
- context.resourceUsage.memory.current = memoryUsage.heapUsed;
3246
+ const memoryBaseline = this.memoryBaselines.get(pluginId) ?? 0;
3247
+ const memoryDelta = Math.max(0, memoryUsage.heapUsed - memoryBaseline);
3248
+ context.resourceUsage.memory.current = memoryDelta;
3174
3249
  context.resourceUsage.memory.peak = Math.max(
3175
3250
  context.resourceUsage.memory.peak,
3176
- memoryUsage.heapUsed
3251
+ memoryDelta
3177
3252
  );
3178
- context.resourceUsage.cpu.current = 0;
3253
+ const cpuBaseline = this.cpuBaselines.get(pluginId) ?? { user: 0, system: 0 };
3254
+ const cpuCurrent = process.cpuUsage();
3255
+ const cpuDeltaUser = cpuCurrent.user - cpuBaseline.user;
3256
+ const cpuDeltaSystem = cpuCurrent.system - cpuBaseline.system;
3257
+ const totalCpuMicros = cpuDeltaUser + cpuDeltaSystem;
3258
+ const intervalMicros = _PluginSandboxRuntime.MONITORING_INTERVAL_MS * 1e3;
3259
+ context.resourceUsage.cpu.current = totalCpuMicros / intervalMicros * 100;
3260
+ this.cpuBaselines.set(pluginId, cpuCurrent);
3179
3261
  const { withinLimits, violations } = this.checkResourceLimits(pluginId);
3180
3262
  if (!withinLimits) {
3181
3263
  this.logger.warn("Resource limit violations detected", {
@@ -3198,9 +3280,13 @@ var PluginSandboxRuntime = class {
3198
3280
  this.stopResourceMonitoring(pluginId);
3199
3281
  }
3200
3282
  this.sandboxes.clear();
3283
+ this.memoryBaselines.clear();
3284
+ this.cpuBaselines.clear();
3201
3285
  this.logger.info("Sandbox runtime shutdown complete");
3202
3286
  }
3203
3287
  };
3288
+ _PluginSandboxRuntime.MONITORING_INTERVAL_MS = 5e3;
3289
+ var PluginSandboxRuntime = _PluginSandboxRuntime;
3204
3290
 
3205
3291
  // src/security/security-scanner.ts
3206
3292
  var PluginSecurityScanner = class {
@@ -3667,6 +3753,7 @@ var PluginHealthMonitor = class {
3667
3753
  };
3668
3754
 
3669
3755
  // src/hot-reload.ts
3756
+ import { createHash } from "crypto";
3670
3757
  var generateUUID = () => {
3671
3758
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
3672
3759
  return crypto.randomUUID();
@@ -3757,19 +3844,11 @@ var PluginStateManager = class {
3757
3844
  this.logger.debug("State cleared", { pluginId });
3758
3845
  }
3759
3846
  /**
3760
- * Calculate simple checksum for state verification
3761
- * WARNING: This is a simple hash for demo purposes.
3762
- * In production, use a cryptographic hash like SHA-256.
3847
+ * Calculate checksum for state verification using SHA-256.
3763
3848
  */
3764
3849
  calculateChecksum(state) {
3765
3850
  const stateStr = JSON.stringify(state);
3766
- let hash = 0;
3767
- for (let i = 0; i < stateStr.length; i++) {
3768
- const char = stateStr.charCodeAt(i);
3769
- hash = (hash << 5) - hash + char;
3770
- hash = hash & hash;
3771
- }
3772
- return hash.toString(16);
3851
+ return createHash("sha256").update(stateStr).digest("hex");
3773
3852
  }
3774
3853
  /**
3775
3854
  * Shutdown state manager