@openparachute/vault 0.4.8-rc.8 → 0.4.8

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.
@@ -117,6 +117,39 @@ describe("self-register", () => {
117
117
  });
118
118
  });
119
119
 
120
+ test("health path follows the primary vault name (closes vault#369)", () => {
121
+ // Regression: pre-fix self-register used `manifest.health` verbatim
122
+ // (which is `/vault/default/health`) regardless of the actual vault
123
+ // name. A hub-bundled wizard that lets the operator name their vault
124
+ // `notes` produced a services.json entry with `paths: ["/vault/notes"]`
125
+ // but `health: "/vault/default/health"`, so hub's per-module health
126
+ // probe hit a 404 even on a healthy vault. The fix derives health from
127
+ // paths[0]. Caught in the wild on a Render rebuild walkthrough.
128
+ withParachuteHome((home) => {
129
+ const { log, warn } = captureLogs();
130
+ selfRegister({
131
+ version: "0.4.8-rc.3",
132
+ log,
133
+ warn,
134
+ readManifest: () => TEST_MANIFEST,
135
+ resolvePackageRoot: () => "/fake/install/dir",
136
+ // Vault named something other than "default" — the manifest fallback
137
+ // path `/vault/default` should NOT leak into the health URL.
138
+ listVaults: () => ["notes"],
139
+ readGlobalConfig: () => ({ port: 1940, default_vault: "notes" }),
140
+ });
141
+ const parsed = JSON.parse(readFileSync(join(home, "services.json"), "utf8")) as {
142
+ services: { paths: string[]; health: string }[];
143
+ };
144
+ const row = parsed.services[0];
145
+ expect(row.paths).toEqual(["/vault/notes"]);
146
+ expect(row.health).toBe("/vault/notes/health");
147
+ // Sanity: health should never be the literal manifest template
148
+ // when a real vault exists with a different name.
149
+ expect(row.health).not.toBe("/vault/default/health");
150
+ });
151
+ });
152
+
120
153
  test("idempotent — re-running reports no changes", () => {
121
154
  withParachuteHome(() => {
122
155
  const { log, warn } = captureLogs();
@@ -163,6 +163,19 @@ export function selfRegister(deps: SelfRegisterDeps): SelfRegisterResult {
163
163
  const paths = buildVaultServicePaths(globalConfig.default_vault, vaults, manifest.paths);
164
164
  const port = globalConfig.port ?? DEFAULT_PORT;
165
165
 
166
+ // Derive the health path from the primary path (paths[0]) rather than
167
+ // using `manifest.health` verbatim. The manifest's `health` is
168
+ // `/vault/default/health` — a template literal where `default` is the
169
+ // placeholder vault name. When the operator named their vault something
170
+ // other than `default` (e.g. `notes`, `journal`, or — caught in the wild
171
+ // on a Render rebuild — just `vault`), the manifest's `default` doesn't
172
+ // get rewritten and the hub's per-module health probe ends up hitting
173
+ // `/vault/default/health` against a vault that lives at `/vault/<other>/`,
174
+ // returning 404 even when the vault is healthy. Build health off paths[0]
175
+ // so the path follows the primary vault by construction. Closes vault#369.
176
+ const primaryPath = paths[0] ?? "/vault/default";
177
+ const health = `${primaryPath}/health`;
178
+
166
179
  // Build the entry with manifest-sourced metadata (displayName, tagline,
167
180
  // stripPrefix) layered on top of the operationally-determined fields
168
181
  // (port from config, paths from current vault list, version from
@@ -178,7 +191,7 @@ export function selfRegister(deps: SelfRegisterDeps): SelfRegisterResult {
178
191
  name: manifest.manifestName,
179
192
  port,
180
193
  paths,
181
- health: manifest.health,
194
+ health,
182
195
  version: deps.version,
183
196
  installDir,
184
197
  };
@@ -212,7 +225,7 @@ export function selfRegister(deps: SelfRegisterDeps): SelfRegisterResult {
212
225
  priorRow.version !== deps.version ||
213
226
  priorRow.port !== port ||
214
227
  JSON.stringify(priorRow.paths) !== JSON.stringify(paths) ||
215
- priorRow.health !== manifest.health ||
228
+ priorRow.health !== health ||
216
229
  (priorRow as { displayName?: string }).displayName !== manifest.displayName ||
217
230
  (priorRow as { tagline?: string }).tagline !== manifest.tagline ||
218
231
  (priorRow as { stripPrefix?: boolean }).stripPrefix !== manifest.stripPrefix;
package/src/server.ts CHANGED
@@ -293,20 +293,9 @@ const server = Bun.serve({
293
293
  return new Response(null, { status: 204, headers: corsHeaders });
294
294
  }
295
295
 
296
- // Derive client IP. Default: socket IP only (safe for direct-internet
297
- // deployments). If TRUST_PROXY=1 is set, honor X-Forwarded-For — use
298
- // this only when a reverse proxy (Cloudflare Tunnel, nginx, etc.) is
299
- // terminating the connection, otherwise attackers can spoof the header
300
- // to evade per-IP rate limiting.
301
- const trustProxy = process.env.TRUST_PROXY === "1" || process.env.TRUST_PROXY === "true";
302
- const forwardedFor = trustProxy ? req.headers.get("x-forwarded-for") : null;
303
- const clientIp = forwardedFor
304
- ? forwardedFor.split(",")[0]!.trim()
305
- : server.requestIP(req)?.address;
306
-
307
296
  try {
308
297
  const start = Date.now();
309
- const response = await route(req, path, clientIp);
298
+ const response = await route(req, path);
310
299
  const ms = Date.now() - start;
311
300
  console.log(`${req.method} ${path} ${response.status} ${ms}ms`);
312
301
  for (const [k, v] of Object.entries(corsHeaders)) {
package/src/vault-name.ts CHANGED
@@ -2,8 +2,9 @@
2
2
  * Validation for vault names.
3
3
  *
4
4
  * Vault names appear in URLs (`/vault/<name>/mcp`, `/vault/<name>/api/*`),
5
- * the SQLite filename, and the OAuth consent page anything that breaks
6
- * URL routing or filesystem assumptions has to be rejected up front.
5
+ * the SQLite filename, and the JWT audience claim (`aud: vault.<name>`)
6
+ * anything that breaks URL routing or filesystem assumptions has to be
7
+ * rejected up front.
7
8
  *
8
9
  * Rule: lowercase alphanumeric + hyphens or underscores, 2–32 chars, with
9
10
  * `list` reserved. Used by the `init` prompt, the `--vault-name` flag, and