@grainulation/silo 1.0.4 → 1.1.0

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.
@@ -9,10 +9,21 @@ const fs = require("node:fs");
9
9
  const path = require("node:path");
10
10
  const { Store } = require("./store.js");
11
11
 
12
+ const VALID_TYPES = [
13
+ "constraint",
14
+ "factual",
15
+ "estimate",
16
+ "risk",
17
+ "recommendation",
18
+ "feedback",
19
+ ];
20
+ const VALID_EVIDENCE = ["stated", "web", "documented", "tested", "production"];
21
+
12
22
  /**
13
23
  * Normalize a claim to wheat's canonical schema.
14
24
  * Handles legacy fields: tier -> evidence, text -> content.
15
25
  * Fills in missing required fields with sensible defaults.
26
+ * Validates enums, sanitizes content, and ensures provenance.
16
27
  */
17
28
  function normalizeClaim(claim) {
18
29
  const normalized = { ...claim };
@@ -41,6 +52,21 @@ function normalizeClaim(claim) {
41
52
  normalized.resolved_by = normalized.resolved_by || null;
42
53
  normalized.tags = normalized.tags || [];
43
54
 
55
+ // Enum validation: clamp type to allowed set
56
+ if (!VALID_TYPES.includes(normalized.type)) {
57
+ normalized.type = "factual";
58
+ }
59
+
60
+ // Enum validation: clamp evidence to allowed set
61
+ if (!VALID_EVIDENCE.includes(normalized.evidence)) {
62
+ normalized.evidence = "stated";
63
+ }
64
+
65
+ // String sanitization: strip HTML tags from content
66
+ if (typeof normalized.content === "string") {
67
+ normalized.content = normalized.content.replace(/<[^>]*>/g, "");
68
+ }
69
+
44
70
  // Normalize source to object form
45
71
  if (!normalized.source || typeof normalized.source === "string") {
46
72
  normalized.source = {
@@ -53,6 +79,11 @@ function normalizeClaim(claim) {
53
79
  };
54
80
  }
55
81
 
82
+ // Ensure provenance: if source.origin is missing, set default
83
+ if (!normalized.source.origin) {
84
+ normalized.source.origin = "silo-import";
85
+ }
86
+
56
87
  return normalized;
57
88
  }
58
89
 
@@ -176,4 +207,4 @@ class ImportExport {
176
207
  }
177
208
  }
178
209
 
179
- module.exports = { ImportExport, normalizeClaim };
210
+ module.exports = { ImportExport, normalizeClaim, VALID_TYPES, VALID_EVIDENCE };
package/lib/packs.js CHANGED
@@ -140,7 +140,20 @@ class Packs {
140
140
  }
141
141
 
142
142
  const pack = JSON.parse(fs.readFileSync(filePath, "utf-8"));
143
- const slug = (pack.name || path.basename(filePath, ".json"))
143
+
144
+ // Validate pack name against path traversal
145
+ const rawName = pack.name || path.basename(filePath, ".json");
146
+ if (
147
+ /[/\\]/.test(rawName) ||
148
+ rawName.includes("..") ||
149
+ rawName.startsWith(".")
150
+ ) {
151
+ throw new Error(
152
+ `Invalid pack name "${rawName}": must not contain /, \\, .., or start with .`,
153
+ );
154
+ }
155
+
156
+ const slug = rawName
144
157
  .toLowerCase()
145
158
  .replace(/[^a-z0-9]+/g, "-")
146
159
  .replace(/^-|-$/g, "");
package/lib/serve-mcp.js CHANGED
@@ -108,7 +108,14 @@ function toolPull(dir, args) {
108
108
  };
109
109
  }
110
110
 
111
- const targetFile = into || path.join(dir, "claims.json");
111
+ const targetFile = into ? path.resolve(dir, into) : path.join(dir, "claims.json");
112
+ // Prevent path traversal — target must stay within workspace
113
+ if (targetFile !== dir && !targetFile.startsWith(dir + path.sep)) {
114
+ return {
115
+ status: "error",
116
+ message: `Target path escapes workspace directory.`,
117
+ };
118
+ }
112
119
  if (!fs.existsSync(targetFile)) {
113
120
  return {
114
121
  status: "error",
@@ -135,7 +142,14 @@ function toolStore(dir, args) {
135
142
  return { status: "error", message: "Required field: name" };
136
143
  }
137
144
 
138
- const sourceFile = from || path.join(dir, "claims.json");
145
+ const sourceFile = from ? path.resolve(dir, from) : path.join(dir, "claims.json");
146
+ // Prevent path traversal — source must stay within workspace
147
+ if (sourceFile !== dir && !sourceFile.startsWith(dir + path.sep)) {
148
+ return {
149
+ status: "error",
150
+ message: `Source path escapes workspace directory.`,
151
+ };
152
+ }
139
153
  if (!fs.existsSync(sourceFile)) {
140
154
  return { status: "error", message: `Source file not found: ${sourceFile}` };
141
155
  }
@@ -247,7 +261,14 @@ function toolConfluence(dir, args) {
247
261
  case "publish": {
248
262
  const { title, from, spaceKey, parentId, pageId } = args;
249
263
  if (!title) return { status: "error", message: "Required field: title" };
250
- const sourceFile = from || path.join(dir, "claims.json");
264
+ const sourceFile = from ? path.resolve(dir, from) : path.join(dir, "claims.json");
265
+ // Prevent path traversal — source must stay within workspace
266
+ if (sourceFile !== dir && !sourceFile.startsWith(dir + path.sep)) {
267
+ return {
268
+ status: "error",
269
+ message: `Source path escapes workspace directory.`,
270
+ };
271
+ }
251
272
  if (!fs.existsSync(sourceFile))
252
273
  return {
253
274
  status: "error",
@@ -269,6 +290,16 @@ function toolConfluence(dir, args) {
269
290
  const target = pid || searchTitle;
270
291
  if (!target)
271
292
  return { status: "error", message: "Required: pageId or title" };
293
+ // Prevent path traversal — target must stay within workspace
294
+ if (into) {
295
+ const checkPath = path.resolve(dir, into);
296
+ if (checkPath !== dir && !checkPath.startsWith(dir + path.sep)) {
297
+ return {
298
+ status: "error",
299
+ message: `Target path escapes workspace directory.`,
300
+ };
301
+ }
302
+ }
272
303
  return confluence
273
304
  .pull(target, { spaceKey })
274
305
  .then((result) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grainulation/silo",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "Reusable knowledge for research sprints -- shared claim libraries, templates, and knowledge packs",
5
5
  "main": "lib/index.js",
6
6
  "exports": {