@openparachute/vault 0.7.2-rc.3 → 0.7.2-rc.5
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/core/src/aggregate.test.ts +23 -0
- package/core/src/contract-taxonomy.test.ts +22 -0
- package/core/src/core.test.ts +384 -0
- package/core/src/query-operators.ts +56 -0
- package/core/src/schema-defaults.ts +17 -2
- package/core/src/store.ts +353 -56
- package/core/src/tag-hierarchy.ts +13 -0
- package/core/src/wikilinks.test.ts +175 -0
- package/core/src/wikilinks.ts +180 -10
- package/package.json +1 -1
- package/src/routes.ts +143 -5
- package/src/vault.test.ts +139 -1
package/src/vault.test.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { BunStore } from "./vault-store.ts";
|
|
|
11
11
|
import { generateMcpTools } from "../core/src/mcp.ts";
|
|
12
12
|
import { getLinksHydrated } from "../core/src/links.ts";
|
|
13
13
|
import { buildVaultProjection } from "../core/src/vault-projection.ts";
|
|
14
|
-
import { handleNotes, handleTags, handleFindPath, handleVault, handleUnresolvedWikilinks } from "./routes.ts";
|
|
14
|
+
import { handleNotes, handleTags, handleFindPath, handleVault, handleUnresolvedWikilinks, MAX_JSON_BODY_BYTES } from "./routes.ts";
|
|
15
15
|
import { expandTokenTagScope } from "./tag-scope.ts";
|
|
16
16
|
import type { TagScopeCtx } from "./routes.ts";
|
|
17
17
|
import { extractApiKey } from "./auth.ts";
|
|
@@ -4198,6 +4198,144 @@ describe("HTTP /notes", async () => {
|
|
|
4198
4198
|
});
|
|
4199
4199
|
});
|
|
4200
4200
|
|
|
4201
|
+
// ---------------------------------------------------------------------------
|
|
4202
|
+
// REST transport hardening — malformed/wrong-shape/oversize JSON bodies
|
|
4203
|
+
// (LB7). Before the fix: malformed JSON on POST /notes, POST
|
|
4204
|
+
// /notes/:id/attachments, and PUT /tags/:name threw past the handler into
|
|
4205
|
+
// server.ts's generic top-level catch — a 500 with no `error_type` (LB7a),
|
|
4206
|
+
// unlike the already-hardened /tags/merge and /tags/:name/rename routes,
|
|
4207
|
+
// which returned a clean 400 `invalid_json`. A syntactically-valid but
|
|
4208
|
+
// wrong-shape body (`null`, `42`, `[]`) either threw a TypeError deep in the
|
|
4209
|
+
// handler or sailed through property access as `undefined`, silently
|
|
4210
|
+
// creating a blank note / no-op update (LB7b). And nothing capped request
|
|
4211
|
+
// body size, so an oversized `content` field reached the store unbounded
|
|
4212
|
+
// (LB7c). Every assertion here MUST fail without the fix (parseJsonBody).
|
|
4213
|
+
// ---------------------------------------------------------------------------
|
|
4214
|
+
describe("REST transport hardening — malformed/wrong-shape/oversize JSON bodies (LB7)", async () => {
|
|
4215
|
+
/** Raw (non-JSON.stringify'd) request body — for malformed-JSON cases mkReq can't express. */
|
|
4216
|
+
function mkRawReq(method: string, path: string, rawBody: string): Request {
|
|
4217
|
+
return new Request(`${BASE}${path}`, {
|
|
4218
|
+
method,
|
|
4219
|
+
body: rawBody,
|
|
4220
|
+
headers: { "Content-Type": "application/json" },
|
|
4221
|
+
});
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
describe("malformed JSON -> clean 400 invalid_json, not a 500 (LB7a)", () => {
|
|
4225
|
+
test("POST /notes", async () => {
|
|
4226
|
+
const res = await handleNotes(mkRawReq("POST", "/notes", "{not valid json"), store, "");
|
|
4227
|
+
expect(res.status).toBe(400);
|
|
4228
|
+
const body = await res.json() as any;
|
|
4229
|
+
expect(body.error_type).toBe("invalid_json");
|
|
4230
|
+
});
|
|
4231
|
+
|
|
4232
|
+
test("POST /notes/:id/attachments", async () => {
|
|
4233
|
+
await store.createNote("x", { id: "att-malformed" });
|
|
4234
|
+
const res = await handleNotes(
|
|
4235
|
+
mkRawReq("POST", "/notes/att-malformed/attachments", "{not valid json"),
|
|
4236
|
+
store,
|
|
4237
|
+
"/att-malformed/attachments",
|
|
4238
|
+
);
|
|
4239
|
+
expect(res.status).toBe(400);
|
|
4240
|
+
const body = await res.json() as any;
|
|
4241
|
+
expect(body.error_type).toBe("invalid_json");
|
|
4242
|
+
});
|
|
4243
|
+
|
|
4244
|
+
test("PATCH /notes/:idOrPath", async () => {
|
|
4245
|
+
await store.createNote("x", { id: "patch-malformed" });
|
|
4246
|
+
const res = await handleNotes(
|
|
4247
|
+
mkRawReq("PATCH", "/notes/patch-malformed", "{not valid json"),
|
|
4248
|
+
store,
|
|
4249
|
+
"/patch-malformed",
|
|
4250
|
+
);
|
|
4251
|
+
expect(res.status).toBe(400);
|
|
4252
|
+
const body = await res.json() as any;
|
|
4253
|
+
expect(body.error_type).toBe("invalid_json");
|
|
4254
|
+
});
|
|
4255
|
+
|
|
4256
|
+
test("PUT /tags/:name — parity with the already-hardened /tags/merge shape", async () => {
|
|
4257
|
+
const res = await handleTags(mkRawReq("PUT", "/tags/malformed", "{not valid json"), store, "/malformed");
|
|
4258
|
+
expect(res.status).toBe(400);
|
|
4259
|
+
const body = await res.json() as any;
|
|
4260
|
+
expect(body.error_type).toBe("invalid_json");
|
|
4261
|
+
});
|
|
4262
|
+
|
|
4263
|
+
test("PATCH /vault-info", async () => {
|
|
4264
|
+
const cfg = { name: "default" } as { name: string };
|
|
4265
|
+
const res = await handleVault(mkRawReq("PATCH", "/vault", "{not valid json"), store, cfg as any);
|
|
4266
|
+
expect(res.status).toBe(400);
|
|
4267
|
+
const body = await res.json() as any;
|
|
4268
|
+
expect(body.error_type).toBe("invalid_json");
|
|
4269
|
+
});
|
|
4270
|
+
});
|
|
4271
|
+
|
|
4272
|
+
// Wrong-SHAPE bodies parse fine as JSON but aren't the object a route
|
|
4273
|
+
// expects — the taxonomy calls that `invalid_request` (parallel to
|
|
4274
|
+
// /tags/merge's sources/target shape errors), NOT `invalid_json` (which is
|
|
4275
|
+
// reserved for a genuine parse failure, asserted in the LB7a block above).
|
|
4276
|
+
describe("wrong-shape (but syntactically valid) JSON body -> 400 invalid_request, not 500 or a silent blank write (LB7b)", () => {
|
|
4277
|
+
test("POST /notes with a `null` body -> 400 invalid_request, not a 500 TypeError", async () => {
|
|
4278
|
+
const res = await handleNotes(mkReq("POST", "/notes", null), store, "");
|
|
4279
|
+
expect(res.status).toBe(400);
|
|
4280
|
+
const body = await res.json() as any;
|
|
4281
|
+
expect(body.error_type).toBe("invalid_request");
|
|
4282
|
+
});
|
|
4283
|
+
|
|
4284
|
+
test("POST /notes with a bare number body -> 400 invalid_request, not a silently-created blank note", async () => {
|
|
4285
|
+
const res = await handleNotes(mkReq("POST", "/notes", 42), store, "");
|
|
4286
|
+
expect(res.status).toBe(400);
|
|
4287
|
+
const body = await res.json() as any;
|
|
4288
|
+
expect(body.error_type).toBe("invalid_request");
|
|
4289
|
+
const after = await (await handleNotes(mkReq("GET", "/notes"), store, "")).json() as any[];
|
|
4290
|
+
expect(after).toHaveLength(0); // no blank note landed
|
|
4291
|
+
});
|
|
4292
|
+
|
|
4293
|
+
test("POST /notes with a bare array body -> 400 invalid_request, not a silently-created blank note", async () => {
|
|
4294
|
+
const res = await handleNotes(mkReq("POST", "/notes", []), store, "");
|
|
4295
|
+
expect(res.status).toBe(400);
|
|
4296
|
+
const body = await res.json() as any;
|
|
4297
|
+
expect(body.error_type).toBe("invalid_request");
|
|
4298
|
+
const after = await (await handleNotes(mkReq("GET", "/notes"), store, "")).json() as any[];
|
|
4299
|
+
expect(after).toHaveLength(0);
|
|
4300
|
+
});
|
|
4301
|
+
|
|
4302
|
+
test("POST /notes with notes: \"not-an-array\" -> 400 invalid_request, not per-character blank notes", async () => {
|
|
4303
|
+
const res = await handleNotes(mkReq("POST", "/notes", { notes: "oops" }), store, "");
|
|
4304
|
+
expect(res.status).toBe(400);
|
|
4305
|
+
const body = await res.json() as any;
|
|
4306
|
+
expect(body.error_type).toBe("invalid_request");
|
|
4307
|
+
expect(body.field).toBe("notes");
|
|
4308
|
+
const after = await (await handleNotes(mkReq("GET", "/notes"), store, "")).json() as any[];
|
|
4309
|
+
expect(after).toHaveLength(0);
|
|
4310
|
+
});
|
|
4311
|
+
|
|
4312
|
+
test("POST /notes with a non-object item inside notes[] -> 400 invalid_request", async () => {
|
|
4313
|
+
const res = await handleNotes(
|
|
4314
|
+
mkReq("POST", "/notes", { notes: [{ content: "ok", path: "ok-one" }, 42] }),
|
|
4315
|
+
store,
|
|
4316
|
+
"",
|
|
4317
|
+
);
|
|
4318
|
+
expect(res.status).toBe(400);
|
|
4319
|
+
const body = await res.json() as any;
|
|
4320
|
+
expect(body.error_type).toBe("invalid_request");
|
|
4321
|
+
const after = await (await handleNotes(mkReq("GET", "/notes"), store, "")).json() as any[];
|
|
4322
|
+
expect(after).toHaveLength(0); // the whole batch is rejected, not a partial write
|
|
4323
|
+
});
|
|
4324
|
+
});
|
|
4325
|
+
|
|
4326
|
+
describe("oversize request body -> 413, not an unbounded write (LB7c)", () => {
|
|
4327
|
+
test("POST /notes with an oversized content field is rejected", async () => {
|
|
4328
|
+
const huge = "x".repeat(MAX_JSON_BODY_BYTES + 1024);
|
|
4329
|
+
const res = await handleNotes(mkReq("POST", "/notes", { content: huge, path: "huge-note" }), store, "");
|
|
4330
|
+
expect(res.status).toBe(413);
|
|
4331
|
+
const body = await res.json() as any;
|
|
4332
|
+
expect(body.error_type).toBe("payload_too_large");
|
|
4333
|
+
const stored = await store.getNoteByPath("huge-note");
|
|
4334
|
+
expect(stored).toBeNull();
|
|
4335
|
+
});
|
|
4336
|
+
});
|
|
4337
|
+
});
|
|
4338
|
+
|
|
4201
4339
|
// ---------------------------------------------------------------------------
|
|
4202
4340
|
// REST tag-scope confidentiality (security review). expand_links must not
|
|
4203
4341
|
// inline out-of-scope wikilinked content; include_links must not hydrate
|