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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "type": "module",
5
5
  "description": "Shared core: capture, index, retrieve, tools, and utilities for context-vault",
6
6
  "main": "src/index.js",
@@ -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: v6 (multi-tenancy)`,
633
+ `Schema: v7 (teams)`,
558
634
  ];
559
635
 
560
636
  if (status.embeddingStatus) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-vault",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "type": "module",
5
5
  "description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
6
6
  "bin": {