context-vault 2.5.1 → 2.6.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.
|
@@ -10,7 +10,7 @@ import { formatFrontmatter } from "../core/frontmatter.js";
|
|
|
10
10
|
import { slugify, kindToPath } from "../core/files.js";
|
|
11
11
|
import { formatBody } from "./formatters.js";
|
|
12
12
|
|
|
13
|
-
function safeFolderPath(vaultDir, kind, folder) {
|
|
13
|
+
export function safeFolderPath(vaultDir, kind, folder) {
|
|
14
14
|
const base = resolve(vaultDir, kindToPath(kind));
|
|
15
15
|
if (!folder) return base;
|
|
16
16
|
const resolved = resolve(base, folder);
|
|
@@ -18,6 +18,64 @@ import { normalizeKind } from "../core/files.js";
|
|
|
18
18
|
import { ok, err, ensureVaultExists, ensureValidKind } from "./helpers.js";
|
|
19
19
|
import { isEmbedAvailable } from "../index/embed.js";
|
|
20
20
|
|
|
21
|
+
// ─── Input size limits (mirrors hosted validation) ────────────────────────────
|
|
22
|
+
const MAX_BODY_LENGTH = 100 * 1024; // 100KB
|
|
23
|
+
const MAX_TITLE_LENGTH = 500;
|
|
24
|
+
const MAX_KIND_LENGTH = 64;
|
|
25
|
+
const MAX_TAG_LENGTH = 100;
|
|
26
|
+
const MAX_TAGS_COUNT = 20;
|
|
27
|
+
const MAX_META_LENGTH = 10 * 1024; // 10KB
|
|
28
|
+
const MAX_SOURCE_LENGTH = 200;
|
|
29
|
+
const MAX_IDENTITY_KEY_LENGTH = 200;
|
|
30
|
+
const MAX_URL_LENGTH = 2048;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate input fields for save_context. Returns an error response or null.
|
|
34
|
+
*/
|
|
35
|
+
function validateSaveInput({ kind, title, body, tags, meta, source, identity_key }) {
|
|
36
|
+
if (kind !== undefined && kind !== null) {
|
|
37
|
+
if (typeof kind !== "string" || kind.length > MAX_KIND_LENGTH) {
|
|
38
|
+
return err(`kind must be a string, max ${MAX_KIND_LENGTH} chars`, "INVALID_INPUT");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (body !== undefined && body !== null) {
|
|
42
|
+
if (typeof body !== "string" || body.length > MAX_BODY_LENGTH) {
|
|
43
|
+
return err(`body must be a string, max ${MAX_BODY_LENGTH / 1024}KB`, "INVALID_INPUT");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (title !== undefined && title !== null) {
|
|
47
|
+
if (typeof title !== "string" || title.length > MAX_TITLE_LENGTH) {
|
|
48
|
+
return err(`title must be a string, max ${MAX_TITLE_LENGTH} chars`, "INVALID_INPUT");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (tags !== undefined && tags !== null) {
|
|
52
|
+
if (!Array.isArray(tags)) return err("tags must be an array of strings", "INVALID_INPUT");
|
|
53
|
+
if (tags.length > MAX_TAGS_COUNT) return err(`tags: max ${MAX_TAGS_COUNT} tags allowed`, "INVALID_INPUT");
|
|
54
|
+
for (const tag of tags) {
|
|
55
|
+
if (typeof tag !== "string" || tag.length > MAX_TAG_LENGTH) {
|
|
56
|
+
return err(`each tag must be a string, max ${MAX_TAG_LENGTH} chars`, "INVALID_INPUT");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (meta !== undefined && meta !== null) {
|
|
61
|
+
const metaStr = JSON.stringify(meta);
|
|
62
|
+
if (metaStr.length > MAX_META_LENGTH) {
|
|
63
|
+
return err(`meta must be under ${MAX_META_LENGTH / 1024}KB when serialized`, "INVALID_INPUT");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (source !== undefined && source !== null) {
|
|
67
|
+
if (typeof source !== "string" || source.length > MAX_SOURCE_LENGTH) {
|
|
68
|
+
return err(`source must be a string, max ${MAX_SOURCE_LENGTH} chars`, "INVALID_INPUT");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (identity_key !== undefined && identity_key !== null) {
|
|
72
|
+
if (typeof identity_key !== "string" || identity_key.length > MAX_IDENTITY_KEY_LENGTH) {
|
|
73
|
+
return err(`identity_key must be a string, max ${MAX_IDENTITY_KEY_LENGTH} chars`, "INVALID_INPUT");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
21
79
|
/**
|
|
22
80
|
* Register all MCP tools on the server.
|
|
23
81
|
*
|
|
@@ -251,6 +309,9 @@ export function registerTools(server, ctx) {
|
|
|
251
309
|
const vaultErr = ensureVaultExists(config);
|
|
252
310
|
if (vaultErr) return vaultErr;
|
|
253
311
|
|
|
312
|
+
const inputErr = validateSaveInput({ kind, title, body, tags, meta, source, identity_key });
|
|
313
|
+
if (inputErr) return inputErr;
|
|
314
|
+
|
|
254
315
|
// ── Update mode ──
|
|
255
316
|
if (id) {
|
|
256
317
|
await ensureIndexed();
|
|
@@ -501,6 +562,21 @@ export function registerTools(server, ctx) {
|
|
|
501
562
|
if (vaultErr) return vaultErr;
|
|
502
563
|
|
|
503
564
|
if (!targetUrl?.trim()) return err("Required: url (non-empty string)", "INVALID_INPUT");
|
|
565
|
+
if (targetUrl.length > MAX_URL_LENGTH) return err(`url must be under ${MAX_URL_LENGTH} chars`, "INVALID_INPUT");
|
|
566
|
+
if (kind !== undefined && kind !== null) {
|
|
567
|
+
if (typeof kind !== "string" || kind.length > MAX_KIND_LENGTH) {
|
|
568
|
+
return err(`kind must be a string, max ${MAX_KIND_LENGTH} chars`, "INVALID_INPUT");
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (tags !== undefined && tags !== null) {
|
|
572
|
+
if (!Array.isArray(tags)) return err("tags must be an array of strings", "INVALID_INPUT");
|
|
573
|
+
if (tags.length > MAX_TAGS_COUNT) return err(`tags: max ${MAX_TAGS_COUNT} tags allowed`, "INVALID_INPUT");
|
|
574
|
+
for (const tag of tags) {
|
|
575
|
+
if (typeof tag !== "string" || tag.length > MAX_TAG_LENGTH) {
|
|
576
|
+
return err(`each tag must be a string, max ${MAX_TAG_LENGTH} chars`, "INVALID_INPUT");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
504
580
|
|
|
505
581
|
await ensureIndexed();
|
|
506
582
|
|
|
@@ -554,7 +630,7 @@ export function registerTools(server, ctx) {
|
|
|
554
630
|
`Data dir: ${config.dataDir}`,
|
|
555
631
|
`Config: ${config.configPath}`,
|
|
556
632
|
`Resolved via: ${status.resolvedFrom}`,
|
|
557
|
-
`Schema:
|
|
633
|
+
`Schema: v7 (teams)`,
|
|
558
634
|
];
|
|
559
635
|
|
|
560
636
|
if (status.embeddingStatus) {
|