vault-cortex 0.13.0 → 0.13.1-beta.64

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/env.js CHANGED
@@ -15,7 +15,7 @@ const LOCAL_OPTIONAL_BLOCK = `# Optional ─────────────
15
15
  # "npx vault-cortex@latest restart" (plain docker restart does not
16
16
  # re-read this file).
17
17
 
18
- # Public URL for OAuth issuer URL in discovery metadata (default: http://localhost:8000).
18
+ # Public URL for OAuth issuer and access-token binding (default: http://localhost:8000).
19
19
  # Override if you expose the server on a different URL (e.g. via a reverse proxy).
20
20
  PUBLIC_URL=http://localhost:8000
21
21
 
@@ -77,9 +77,10 @@ MEMORY_DIR=About Me
77
77
  # DAILY_NOTES_FOLDER=Journal
78
78
  # DAILY_NOTES_FORMAT=YYYY-MM-DD
79
79
 
80
- # Comma-separated folders protected from deletion (default: MEMORY_DIR plus
81
- # the daily notes folder DAILY_NOTES_FOLDER when set, otherwise "Daily Notes").
82
- # A custom folder set only in daily-notes.json is not auto-protected.
80
+ # Comma-separated folders protected from deletion and moves. Default: MEMORY_DIR plus
81
+ # the daily notes folder, read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json
82
+ # (default "Daily Notes"). When set, replaces the whole default include the
83
+ # daily notes folder in your list if needed.
83
84
  # PROTECTED_PATHS=About Me,Daily Notes
84
85
 
85
86
  # Comma-separated folders excluded from orphan detection (default: the daily
@@ -215,9 +216,10 @@ MEMORY_DIR=About Me
215
216
  # DAILY_NOTES_FOLDER=Journal
216
217
  # DAILY_NOTES_FORMAT=YYYY-MM-DD
217
218
 
218
- # Comma-separated folders protected from deletion (default: MEMORY_DIR plus
219
- # the daily notes folder DAILY_NOTES_FOLDER when set, otherwise "Daily Notes").
220
- # A custom folder set only in daily-notes.json is not auto-protected.
219
+ # Comma-separated folders protected from deletion and moves. Default: MEMORY_DIR plus
220
+ # the daily notes folder, read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json
221
+ # (default "Daily Notes"). When set, replaces the whole default include the
222
+ # daily notes folder in your list if needed.
221
223
  # PROTECTED_PATHS=About Me,Daily Notes
222
224
 
223
225
  # Comma-separated folders excluded from orphan detection (default: the daily
@@ -339,7 +341,8 @@ VAULT_PASSWORD=${answers.vaultPassword}`;
339
341
  MCP_AUTH_TOKEN=${answers.mcpAuthToken}
340
342
 
341
343
  # Public URL that MCP clients use to reach this server.
342
- # Used as the OAuth issuer URL in discovery metadata.
344
+ # Used as the OAuth issuer URL in discovery metadata and stamped on every
345
+ # access token; changing it invalidates connected clients' tokens.
343
346
  PUBLIC_URL=${answers.publicUrl}
344
347
 
345
348
  ${obsidianTokenComment}
package/dist/init.js CHANGED
@@ -86,28 +86,52 @@ const parseHttpUrl = (value) => {
86
86
  return null;
87
87
  }
88
88
  };
89
+ /** Validates a PUBLIC_URL value: must be http(s), no credentials, no /mcp suffix. */
90
+ export const validatePublicUrl = (input) => {
91
+ const trimmed = input.trim();
92
+ const url = parseHttpUrl(trimmed);
93
+ if (!url) {
94
+ return {
95
+ kind: "error",
96
+ message: "PUBLIC_URL must be a full http:// or https:// URL (e.g. https://vault.example.com).",
97
+ };
98
+ }
99
+ if (url.username || url.password) {
100
+ return {
101
+ kind: "error",
102
+ message: "PUBLIC_URL must not contain credentials (user:password@).",
103
+ };
104
+ }
105
+ // Raw-string check: url.search/url.hash return "" for bare delimiters
106
+ // (WHATWG spec treats empty-string and null query/fragment identically),
107
+ // so a parsed-property check misses "https://host/?" and "https://host/#".
108
+ if (trimmed.includes("?") || trimmed.includes("#")) {
109
+ return {
110
+ kind: "error",
111
+ message: "PUBLIC_URL must be a bare origin or path — no query string (?...) or fragment (#...).",
112
+ };
113
+ }
114
+ if (TRAILING_MCP_PATH.test(url.pathname)) {
115
+ return {
116
+ kind: "error",
117
+ message: "Leave /mcp off PUBLIC_URL — it's the base URL and the server adds /mcp itself (e.g. https://vault.example.com).",
118
+ };
119
+ }
120
+ // Trim trailing slashes so the connect URL is `${base}/mcp`, never
121
+ // `${base}//mcp` — URL.href/.origin don't round-trip reverse-proxy subpaths.
122
+ return { kind: "ok", url: trimmed.replace(/\/+$/, "") };
123
+ };
89
124
  /** Re-prompts until the answer is a valid base http(s) URL (no /mcp path). */
90
125
  const askPublicUrl = async (prompts) => {
91
126
  const answer = await prompts.text("Public base URL clients will use to reach this server (no /mcp — it's added for you):", {
92
127
  placeholder: "https://vault.example.com or http://203.0.113.10:8000",
93
128
  });
94
- const trimmed = answer.trim();
95
- const url = parseHttpUrl(trimmed);
96
- if (url === null) {
97
- prompts.error("PUBLIC_URL must be a full http:// or https:// URL (e.g. https://vault.example.com).");
98
- return askPublicUrl(prompts);
99
- }
100
- // Reject a re-included endpoint path instead of stripping it silently —
101
- // PUBLIC_URL is the base origin and the server adds /mcp itself.
102
- if (TRAILING_MCP_PATH.test(url.pathname)) {
103
- prompts.error("Leave /mcp off PUBLIC_URL — it's the base URL and the server adds /mcp itself (e.g. https://vault.example.com).");
129
+ const result = validatePublicUrl(answer);
130
+ if (result.kind === "error") {
131
+ prompts.error(result.message);
104
132
  return askPublicUrl(prompts);
105
133
  }
106
- // Store the input as typed, trimming only a trailing slash so the connect
107
- // URL is `${base}/mcp`, never `${base}//mcp`. URL's own normalization is
108
- // unusable here: `.href` adds a trailing slash and `.origin` drops the path,
109
- // so neither round-trips a reverse-proxy subpath like https://host/api.
110
- return trimmed.replace(/\/+$/, "");
134
+ return result.url;
111
135
  };
112
136
  /** Re-prompts until non-empty. */
113
137
  const askVaultName = async (prompts) => {
@@ -395,11 +419,8 @@ export const runInit = async (flags, deps) => {
395
419
  prompts.intro("vault-cortex init");
396
420
  // Mode resolution: explicit --mode wins; --yes implies local; otherwise
397
421
  // ask, defaulting to local — it's the simpler activation path.
398
- const mode = flags.mode !== undefined && isMode(flags.mode)
399
- ? flags.mode
400
- : flags.yes
401
- ? "local"
402
- : await askMode(prompts);
422
+ const flagMode = flags.mode && isMode(flags.mode) ? flags.mode : undefined;
423
+ const mode = flagMode ?? (flags.yes ? "local" : await askMode(prompts));
403
424
  const exitCode = mode === "local"
404
425
  ? await runLocalInit(flags, deps)
405
426
  : await runRemoteInit(flags, deps);
package/dist/messages.js CHANGED
@@ -126,11 +126,10 @@ const updateGuidance = (targetDir) => `Update to the latest release:
126
126
  export const buildLocalConnectMessage = (params) => {
127
127
  const { targetDir, token, startStatus, port, tokenWritten } = params;
128
128
  const baseUrl = `http://localhost:${port}`;
129
- const startLine = startStatus === "running"
130
- ? "The server is running."
131
- : startStatus === "starting"
132
- ? startingInBackgroundLine()
133
- : startServerLine(targetDir);
129
+ const nonRunningLine = startStatus === "starting"
130
+ ? startingInBackgroundLine()
131
+ : startServerLine(targetDir);
132
+ const startLine = startStatus === "running" ? "The server is running." : nonRunningLine;
134
133
  const tokenLine = tokenBlock({ targetDir, token, tokenWritten });
135
134
  // Once the server is confirmed up, the smoke test is dropped — the CLI just
136
135
  // verified this exact URL, so re-printing it reads as leftover homework.
@@ -1,4 +1,6 @@
1
1
  import { DEFAULT_PORT } from "./scaffold.js";
2
+ /** Matches Moment.js [...] literal escape groups, splitting format spans from literal content. */
3
+ const MOMENT_BRACKET_ESCAPE = /\[([^\]]*)\]/g;
2
4
  // The curated prompt set — settings users most often want without reading
3
5
  // .env comments. Everything else stays documented-only in the generated
4
6
  // optional block, deliberately: every extra prompt costs init flow length.
@@ -23,6 +25,13 @@ const OPTIONAL_SETTINGS = [
23
25
  label: "Daily notes folder",
24
26
  question: "Vault folder for daily notes:",
25
27
  placeholder: "blank = use your vault's daily notes settings",
28
+ validate: (value) => {
29
+ if (value.includes(".."))
30
+ return "Path traversal (..) is not allowed in folder names.";
31
+ if (value.startsWith("/"))
32
+ return "Absolute paths are not allowed — use a vault-relative folder name.";
33
+ return undefined;
34
+ },
26
35
  },
27
36
  {
28
37
  kind: "optionalText",
@@ -30,6 +39,21 @@ const OPTIONAL_SETTINGS = [
30
39
  label: "Daily notes format",
31
40
  question: "Filename date format for daily notes (e.g. YYYY-MM-DD):",
32
41
  placeholder: "blank = use your vault's daily notes settings",
42
+ validate: (value) => {
43
+ if (value.includes(".."))
44
+ return "Date format must not contain path traversal (..).";
45
+ if (value.startsWith("/"))
46
+ return "Date format must not start with a path separator.";
47
+ if (value.endsWith("/"))
48
+ return "Date format must not end with a path separator.";
49
+ // Moment format tokens are all letters — digits outside of [...]
50
+ // bracket escapes are almost always a mistake.
51
+ const formatSegments = value.split(MOMENT_BRACKET_ESCAPE);
52
+ const hasDigitsInFormat = formatSegments.some((segment, index) => index % 2 === 0 && /\d/.test(segment));
53
+ if (hasDigitsInFormat)
54
+ return "Date format should use Moment tokens (YYYY, MM, DD), not digits — wrap literal text in [...] brackets.";
55
+ return undefined;
56
+ },
33
57
  },
34
58
  {
35
59
  kind: "toggle",
@@ -193,8 +217,17 @@ const askFolder = async (params, prompts) => {
193
217
  defaultValue: currentValue ?? defaultValue,
194
218
  placeholder: defaultValue,
195
219
  })).trim();
196
- if (answer !== "")
220
+ if (answer !== "") {
221
+ if (answer.includes("..")) {
222
+ prompts.error("Path traversal (..) is not allowed in folder names.");
223
+ return askFolder(params, prompts);
224
+ }
225
+ if (answer.startsWith("/")) {
226
+ prompts.error("Absolute paths are not allowed — use a vault-relative folder name.");
227
+ return askFolder(params, prompts);
228
+ }
197
229
  return answer;
230
+ }
198
231
  prompts.error("The folder name can't be empty.");
199
232
  return askFolder(params, prompts);
200
233
  };
@@ -251,12 +284,21 @@ const askSettingValue = async (params, prompts) => {
251
284
  currentValue,
252
285
  defaultValue: setting.defaultValue,
253
286
  }, prompts);
254
- case "optionalText":
255
- return askOptionalText({
287
+ case "optionalText": {
288
+ const value = await askOptionalText({
256
289
  question: setting.question,
257
290
  placeholder: setting.placeholder,
258
291
  currentValue,
259
292
  }, prompts);
293
+ if (value && setting.validate) {
294
+ const error = setting.validate(value);
295
+ if (error) {
296
+ prompts.error(error);
297
+ return askSettingValue(params, prompts);
298
+ }
299
+ }
300
+ return value;
301
+ }
260
302
  case "choice":
261
303
  return prompts.select(setting.question, setting.choices, currentValue ?? setting.defaultValue);
262
304
  }
package/dist/vault.js CHANGED
@@ -21,6 +21,11 @@ export const validateVaultPath = (input) => {
21
21
  const trimmed = input.trim();
22
22
  if (trimmed === "")
23
23
  return { kind: "error", message: "Vault path is required." };
24
+ if (/[*?[]/.test(trimmed))
25
+ return {
26
+ kind: "error",
27
+ message: "Vault path must not contain glob characters (*, ?, [).",
28
+ };
24
29
  const absolutePath = resolve(expandTilde(trimmed));
25
30
  if (!existsSync(absolutePath)) {
26
31
  return { kind: "error", message: `Path does not exist: ${absolutePath}` };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vault-cortex",
3
- "version": "0.13.0",
3
+ "version": "0.13.1-beta.64",
4
4
  "description": "Set up a Vault Cortex MCP server for your Obsidian vault in one command: npx vault-cortex@latest init",
5
5
  "license": "MIT",
6
6
  "type": "module",