Pin the API's body-shape refusal and the partition gate's expiry case

Two more from the routes sweep.

`bodyObject` rejects arrays explicitly because typeof [] is "object" — the
array check is doing real work, not restating the typeof. Without it an array
body reached the handlers, where `body["kind"]` is undefined and the caller
was told a field was missing, sending them after the wrong problem.

The partition gate's test covers the case that actually matters. With no tag
at all, dropping the gate fails anyway because readPartition cannot decrypt
with null — so the mutation survived. But an EXPIRED session still holds its
tag, and without the gate readPartition succeeds with the stale key and
returns the secret: the second factor lapsing silently. No test read a
partition entry through the API at all before this.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
This commit is contained in:
thePR0M3TH3ANandClaude Opus 5 committed 2026-08-19 21:08:59 -04:00
1 parent 5315d8fae9
commit bd17ad5eae
1 file changed
+24
+24
View File
@@ -226,6 +226,30 @@ describe("what the API refuses", () => {
}
});
it("refuses a body that is not a JSON object", async () => {
// typeof [] is "object", so the array check is doing real work rather
// than restating the typeof. Without it an array body reaches the
// handlers, where `body["kind"]` is undefined and the caller is told a
// field is missing — sending them looking for the wrong problem.
for (const raw of ["[1,2,3]", '"a string"', "42", "null"]) {
const res = await call("POST", "/api/v1/entry", {
password: PASSWORD,
raw,
headers: { "content-type": "application/json" },
});
expect(res.status).toBe(400);
expect(res.json.detail).toContain("JSON object");
}
// An object still works, so this is rejecting the shape and not the route.
expect(
(await call("POST", "/api/v1/entry", {
password: PASSWORD,
body: { kind: "password", label: "shape-check" },
})).status,
).toBe(201);
});
it("refuses a config key it does not know, instead of storing it forever", async () => {
// This route used to take the key from the URL and the value from the
// body with no validation at all, so `inactivity_timout` was accepted,