@objectstack/core 2.0.0 → 2.0.2

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/core@2.0.0 build /home/runner/work/spec/spec/packages/core
2
+ > @objectstack/core@2.0.2 build /home/runner/work/spec/spec/packages/core
3
3
  > tsup --config ../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- CJS dist/index.cjs 130.65 KB
14
- CJS dist/index.cjs.map 271.95 KB
15
- CJS ⚡️ Build success in 91ms
16
- ESM dist/index.js 128.03 KB
17
- ESM dist/index.js.map 270.63 KB
18
- ESM ⚡️ Build success in 91ms
13
+ ESM dist/index.js 131.69 KB
14
+ ESM dist/index.js.map 276.68 KB
15
+ ESM ⚡️ Build success in 92ms
16
+ CJS dist/index.cjs 134.46 KB
17
+ CJS dist/index.cjs.map 278.04 KB
18
+ CJS ⚡️ Build success in 97ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 3900ms
21
- DTS dist/index.d.ts 52.46 KB
22
- DTS dist/index.d.cts 52.46 KB
20
+ DTS ⚡️ Build success in 3262ms
21
+ DTS dist/index.d.ts 52.28 KB
22
+ DTS dist/index.d.cts 52.28 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @objectstack/core
2
2
 
3
+ ## 2.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [1db8559]
8
+ - @objectstack/spec@2.0.2
9
+
10
+ ## 2.0.1
11
+
12
+ ### Patch Changes
13
+
14
+ - Patch release for maintenance and stability improvements
15
+ - Updated dependencies
16
+ - @objectstack/spec@2.0.1
17
+
3
18
  ## 2.0.0
4
19
 
5
20
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -2138,8 +2138,18 @@ var TestRunner = class {
2138
2138
  }
2139
2139
  return result;
2140
2140
  }
2141
- resolveVariables(action, _context) {
2142
- return action;
2141
+ resolveVariables(action, context) {
2142
+ const actionStr = JSON.stringify(action);
2143
+ const resolved = actionStr.replace(/\{\{([^}]+)\}\}/g, (_match, varPath) => {
2144
+ const value = this.getValueByPath(context, varPath.trim());
2145
+ if (value === void 0) return _match;
2146
+ return typeof value === "string" ? value : JSON.stringify(value);
2147
+ });
2148
+ try {
2149
+ return JSON.parse(resolved);
2150
+ } catch {
2151
+ return action;
2152
+ }
2143
2153
  }
2144
2154
  getValueByPath(obj, path) {
2145
2155
  if (!path) return obj;
@@ -2476,9 +2486,38 @@ var PluginSignatureVerifier = class {
2476
2486
  return false;
2477
2487
  }
2478
2488
  }
2479
- async verifyCryptoSignatureBrowser(_data, _signature, _publicKey) {
2480
- this.logger.warn("Browser signature verification not yet implemented");
2481
- return false;
2489
+ async verifyCryptoSignatureBrowser(data, signature, publicKey) {
2490
+ try {
2491
+ const subtle = globalThis.crypto?.subtle;
2492
+ if (!subtle) {
2493
+ this.logger.error("SubtleCrypto not available in this environment");
2494
+ return false;
2495
+ }
2496
+ const pemBody = publicKey.replace(/-----BEGIN PUBLIC KEY-----/, "").replace(/-----END PUBLIC KEY-----/, "").replace(/\s/g, "");
2497
+ const keyBytes = Uint8Array.from(atob(pemBody), (c) => c.charCodeAt(0));
2498
+ let importAlgorithm;
2499
+ let verifyAlgorithm;
2500
+ if (this.config.algorithm === "ES256") {
2501
+ importAlgorithm = { name: "ECDSA", namedCurve: "P-256" };
2502
+ verifyAlgorithm = { name: "ECDSA", hash: "SHA-256" };
2503
+ } else {
2504
+ importAlgorithm = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
2505
+ verifyAlgorithm = { name: "RSASSA-PKCS1-v1_5" };
2506
+ }
2507
+ const cryptoKey = await subtle.importKey(
2508
+ "spki",
2509
+ keyBytes,
2510
+ importAlgorithm,
2511
+ false,
2512
+ ["verify"]
2513
+ );
2514
+ const signatureBytes = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0));
2515
+ const dataBytes = new TextEncoder().encode(data);
2516
+ return await subtle.verify(verifyAlgorithm, cryptoKey, signatureBytes, dataBytes);
2517
+ } catch (error) {
2518
+ this.logger.error("Browser signature verification failed", error);
2519
+ return false;
2520
+ }
2482
2521
  }
2483
2522
  validateConfig() {
2484
2523
  if (!this.config.trustedPublicKeys || this.config.trustedPublicKeys.size === 0) {
@@ -2696,29 +2735,48 @@ var PluginPermissionEnforcer = class {
2696
2735
  return false;
2697
2736
  });
2698
2737
  }
2699
- checkFileRead(capabilities, _path) {
2738
+ matchGlob(pattern, str) {
2739
+ const regexStr = pattern.split("**").map((segment) => {
2740
+ const escaped = segment.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
2741
+ return escaped.replace(/\*/g, "[^/]*");
2742
+ }).join(".*");
2743
+ return new RegExp(`^${regexStr}$`).test(str);
2744
+ }
2745
+ checkFileRead(capabilities, path) {
2700
2746
  return capabilities.some((cap) => {
2701
2747
  const protocolId = cap.protocol.id;
2702
2748
  if (protocolId.includes("protocol.filesystem.read")) {
2703
- return true;
2749
+ const paths = cap.metadata?.paths;
2750
+ if (!Array.isArray(paths) || paths.length === 0) {
2751
+ return true;
2752
+ }
2753
+ return paths.some((p) => typeof p === "string" && this.matchGlob(p, path));
2704
2754
  }
2705
2755
  return false;
2706
2756
  });
2707
2757
  }
2708
- checkFileWrite(capabilities, _path) {
2758
+ checkFileWrite(capabilities, path) {
2709
2759
  return capabilities.some((cap) => {
2710
2760
  const protocolId = cap.protocol.id;
2711
2761
  if (protocolId.includes("protocol.filesystem.write")) {
2712
- return true;
2762
+ const paths = cap.metadata?.paths;
2763
+ if (!Array.isArray(paths) || paths.length === 0) {
2764
+ return true;
2765
+ }
2766
+ return paths.some((p) => typeof p === "string" && this.matchGlob(p, path));
2713
2767
  }
2714
2768
  return false;
2715
2769
  });
2716
2770
  }
2717
- checkNetworkAccess(capabilities, _url) {
2771
+ checkNetworkAccess(capabilities, url) {
2718
2772
  return capabilities.some((cap) => {
2719
2773
  const protocolId = cap.protocol.id;
2720
2774
  if (protocolId.includes("protocol.network")) {
2721
- return true;
2775
+ const hosts = cap.metadata?.hosts;
2776
+ if (!Array.isArray(hosts) || hosts.length === 0) {
2777
+ return true;
2778
+ }
2779
+ return hosts.some((h) => typeof h === "string" && this.matchGlob(h, url));
2722
2780
  }
2723
2781
  return false;
2724
2782
  });
@@ -2985,12 +3043,16 @@ var PluginPermissionManager = class {
2985
3043
  };
2986
3044
 
2987
3045
  // src/security/sandbox-runtime.ts
2988
- var PluginSandboxRuntime = class {
3046
+ var import_node_path = __toESM(require("path"), 1);
3047
+ var _PluginSandboxRuntime = class _PluginSandboxRuntime {
2989
3048
  constructor(logger) {
2990
3049
  // Active sandboxes (pluginId -> context)
2991
3050
  this.sandboxes = /* @__PURE__ */ new Map();
2992
3051
  // Resource monitoring intervals
2993
3052
  this.monitoringIntervals = /* @__PURE__ */ new Map();
3053
+ // Per-plugin resource baselines for delta tracking
3054
+ this.memoryBaselines = /* @__PURE__ */ new Map();
3055
+ this.cpuBaselines = /* @__PURE__ */ new Map();
2994
3056
  this.logger = logger.child({ component: "SandboxRuntime" });
2995
3057
  }
2996
3058
  /**
@@ -3011,6 +3073,9 @@ var PluginSandboxRuntime = class {
3011
3073
  }
3012
3074
  };
3013
3075
  this.sandboxes.set(pluginId, context);
3076
+ const baselineMemory = getMemoryUsage();
3077
+ this.memoryBaselines.set(pluginId, baselineMemory.heapUsed);
3078
+ this.cpuBaselines.set(pluginId, process.cpuUsage());
3014
3079
  this.startResourceMonitoring(pluginId);
3015
3080
  this.logger.info("Sandbox created", {
3016
3081
  pluginId,
@@ -3029,6 +3094,8 @@ var PluginSandboxRuntime = class {
3029
3094
  return;
3030
3095
  }
3031
3096
  this.stopResourceMonitoring(pluginId);
3097
+ this.memoryBaselines.delete(pluginId);
3098
+ this.cpuBaselines.delete(pluginId);
3032
3099
  this.sandboxes.delete(pluginId);
3033
3100
  this.logger.info("Sandbox destroyed", { pluginId });
3034
3101
  }
@@ -3056,45 +3123,46 @@ var PluginSandboxRuntime = class {
3056
3123
  }
3057
3124
  /**
3058
3125
  * Check file system access
3059
- * WARNING: Uses simple prefix matching. For production, use proper path
3060
- * resolution with path.resolve() and path.normalize() to prevent traversal.
3126
+ * Uses path.resolve() and path.normalize() to prevent directory traversal.
3061
3127
  */
3062
- checkFileAccess(config, path) {
3128
+ checkFileAccess(config, filePath) {
3063
3129
  if (config.level === "none") {
3064
3130
  return { allowed: true };
3065
3131
  }
3066
3132
  if (!config.filesystem) {
3067
3133
  return { allowed: false, reason: "File system access not configured" };
3068
3134
  }
3069
- if (!path) {
3135
+ if (!filePath) {
3070
3136
  return { allowed: config.filesystem.mode !== "none" };
3071
3137
  }
3072
3138
  const allowedPaths = config.filesystem.allowedPaths || [];
3139
+ const resolvedPath = import_node_path.default.normalize(import_node_path.default.resolve(filePath));
3073
3140
  const isAllowed = allowedPaths.some((allowed) => {
3074
- return path.startsWith(allowed);
3141
+ const resolvedAllowed = import_node_path.default.normalize(import_node_path.default.resolve(allowed));
3142
+ return resolvedPath.startsWith(resolvedAllowed);
3075
3143
  });
3076
3144
  if (allowedPaths.length > 0 && !isAllowed) {
3077
3145
  return {
3078
3146
  allowed: false,
3079
- reason: `Path not in allowed list: ${path}`
3147
+ reason: `Path not in allowed list: ${filePath}`
3080
3148
  };
3081
3149
  }
3082
3150
  const deniedPaths = config.filesystem.deniedPaths || [];
3083
3151
  const isDenied = deniedPaths.some((denied) => {
3084
- return path.startsWith(denied);
3152
+ const resolvedDenied = import_node_path.default.normalize(import_node_path.default.resolve(denied));
3153
+ return resolvedPath.startsWith(resolvedDenied);
3085
3154
  });
3086
3155
  if (isDenied) {
3087
3156
  return {
3088
3157
  allowed: false,
3089
- reason: `Path is explicitly denied: ${path}`
3158
+ reason: `Path is explicitly denied: ${filePath}`
3090
3159
  };
3091
3160
  }
3092
3161
  return { allowed: true };
3093
3162
  }
3094
3163
  /**
3095
3164
  * Check network access
3096
- * WARNING: Uses simple string matching. For production, use proper URL
3097
- * parsing with new URL() and check hostname property.
3165
+ * Uses URL parsing to properly validate hostnames.
3098
3166
  */
3099
3167
  checkNetworkAccess(config, url) {
3100
3168
  if (config.level === "none") {
@@ -3109,10 +3177,16 @@ var PluginSandboxRuntime = class {
3109
3177
  if (!url) {
3110
3178
  return { allowed: config.network.mode !== "none" };
3111
3179
  }
3180
+ let parsedHostname;
3181
+ try {
3182
+ parsedHostname = new URL(url).hostname;
3183
+ } catch {
3184
+ return { allowed: false, reason: `Invalid URL: ${url}` };
3185
+ }
3112
3186
  const allowedHosts = config.network.allowedHosts || [];
3113
3187
  if (allowedHosts.length > 0) {
3114
3188
  const isAllowed = allowedHosts.some((host) => {
3115
- return url.includes(host);
3189
+ return parsedHostname === host;
3116
3190
  });
3117
3191
  if (!isAllowed) {
3118
3192
  return {
@@ -3123,7 +3197,7 @@ var PluginSandboxRuntime = class {
3123
3197
  }
3124
3198
  const deniedHosts = config.network.deniedHosts || [];
3125
3199
  const isDenied = deniedHosts.some((host) => {
3126
- return url.includes(host);
3200
+ return parsedHostname === host;
3127
3201
  });
3128
3202
  if (isDenied) {
3129
3203
  return {
@@ -3200,7 +3274,7 @@ var PluginSandboxRuntime = class {
3200
3274
  startResourceMonitoring(pluginId) {
3201
3275
  const interval = setInterval(() => {
3202
3276
  this.updateResourceUsage(pluginId);
3203
- }, 5e3);
3277
+ }, _PluginSandboxRuntime.MONITORING_INTERVAL_MS);
3204
3278
  this.monitoringIntervals.set(pluginId, interval);
3205
3279
  }
3206
3280
  /**
@@ -3216,10 +3290,9 @@ var PluginSandboxRuntime = class {
3216
3290
  /**
3217
3291
  * Update resource usage statistics
3218
3292
  *
3219
- * NOTE: Currently uses global process.memoryUsage() which tracks the entire
3220
- * Node.js process, not individual plugins. For production, implement proper
3221
- * per-plugin tracking using V8 heap snapshots or allocation tracking at
3222
- * plugin boundaries.
3293
+ * Tracks per-plugin memory and CPU usage using delta from baseline
3294
+ * captured at sandbox creation time. This is an approximation since
3295
+ * true per-plugin isolation isn't possible in a single Node.js process.
3223
3296
  */
3224
3297
  updateResourceUsage(pluginId) {
3225
3298
  const context = this.sandboxes.get(pluginId);
@@ -3227,12 +3300,21 @@ var PluginSandboxRuntime = class {
3227
3300
  return;
3228
3301
  }
3229
3302
  const memoryUsage = getMemoryUsage();
3230
- context.resourceUsage.memory.current = memoryUsage.heapUsed;
3303
+ const memoryBaseline = this.memoryBaselines.get(pluginId) ?? 0;
3304
+ const memoryDelta = Math.max(0, memoryUsage.heapUsed - memoryBaseline);
3305
+ context.resourceUsage.memory.current = memoryDelta;
3231
3306
  context.resourceUsage.memory.peak = Math.max(
3232
3307
  context.resourceUsage.memory.peak,
3233
- memoryUsage.heapUsed
3308
+ memoryDelta
3234
3309
  );
3235
- context.resourceUsage.cpu.current = 0;
3310
+ const cpuBaseline = this.cpuBaselines.get(pluginId) ?? { user: 0, system: 0 };
3311
+ const cpuCurrent = process.cpuUsage();
3312
+ const cpuDeltaUser = cpuCurrent.user - cpuBaseline.user;
3313
+ const cpuDeltaSystem = cpuCurrent.system - cpuBaseline.system;
3314
+ const totalCpuMicros = cpuDeltaUser + cpuDeltaSystem;
3315
+ const intervalMicros = _PluginSandboxRuntime.MONITORING_INTERVAL_MS * 1e3;
3316
+ context.resourceUsage.cpu.current = totalCpuMicros / intervalMicros * 100;
3317
+ this.cpuBaselines.set(pluginId, cpuCurrent);
3236
3318
  const { withinLimits, violations } = this.checkResourceLimits(pluginId);
3237
3319
  if (!withinLimits) {
3238
3320
  this.logger.warn("Resource limit violations detected", {
@@ -3255,9 +3337,13 @@ var PluginSandboxRuntime = class {
3255
3337
  this.stopResourceMonitoring(pluginId);
3256
3338
  }
3257
3339
  this.sandboxes.clear();
3340
+ this.memoryBaselines.clear();
3341
+ this.cpuBaselines.clear();
3258
3342
  this.logger.info("Sandbox runtime shutdown complete");
3259
3343
  }
3260
3344
  };
3345
+ _PluginSandboxRuntime.MONITORING_INTERVAL_MS = 5e3;
3346
+ var PluginSandboxRuntime = _PluginSandboxRuntime;
3261
3347
 
3262
3348
  // src/security/security-scanner.ts
3263
3349
  var PluginSecurityScanner = class {
@@ -3724,6 +3810,7 @@ var PluginHealthMonitor = class {
3724
3810
  };
3725
3811
 
3726
3812
  // src/hot-reload.ts
3813
+ var import_node_crypto = require("crypto");
3727
3814
  var generateUUID = () => {
3728
3815
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
3729
3816
  return crypto.randomUUID();
@@ -3814,19 +3901,11 @@ var PluginStateManager = class {
3814
3901
  this.logger.debug("State cleared", { pluginId });
3815
3902
  }
3816
3903
  /**
3817
- * Calculate simple checksum for state verification
3818
- * WARNING: This is a simple hash for demo purposes.
3819
- * In production, use a cryptographic hash like SHA-256.
3904
+ * Calculate checksum for state verification using SHA-256.
3820
3905
  */
3821
3906
  calculateChecksum(state) {
3822
3907
  const stateStr = JSON.stringify(state);
3823
- let hash = 0;
3824
- for (let i = 0; i < stateStr.length; i++) {
3825
- const char = stateStr.charCodeAt(i);
3826
- hash = (hash << 5) - hash + char;
3827
- hash = hash & hash;
3828
- }
3829
- return hash.toString(16);
3908
+ return (0, import_node_crypto.createHash)("sha256").update(stateStr).digest("hex");
3830
3909
  }
3831
3910
  /**
3832
3911
  * Shutdown state manager