Close four more gaps the widened mutation rule found

Sweeping the smaller targets under the refusal-based selection. All four are
cases where two code paths both refuse, so only the reason or the boundary
was observable — and nothing observed either.

- Approval RESOURCE SCOPING was tested only on its failing side. The check is
  `record !== "*" && record !== requested`; for a scoped record and a
  different resource both halves are true, so `&&` and `||` agree and the
  mutation was invisible. The two cases where they differ are exactly the
  ones that make scoping useful rather than merely restrictive: a scoped
  approval must WORK for the resource it names, and a wildcard must work for
  any resource. Under `||` both stop working entirely.
- The request body cap's boundary. A limit of N bytes has to mean N bytes are
  allowed; `>` accepts exactly N and `>=` rejects it, and the existing test
  sent 500 bytes at a 64-byte cap, which cannot tell them apart. The cap
  could have gone off-by-one and surfaced only as requests failing at a size
  the operator configured as fine.
- readEnvelope's existsSync guard. Without it, ENOENT is caught by the same
  handler as malformed JSON, so a profile that never configured a factor is
  told its partition envelope is invalid — "restore a backup" instead of "set
  one up".
- sinks.ts and server.ts swept clean under the new rule with no further
  changes.

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 20:38:35 -04:00
1 parent 9c9544b725
commit 4184de9b9e
2 files changed
+88 -1

No files matched your search

+24
View File
@@ -312,6 +312,30 @@ describe("what the API refuses", () => {
body: JSON.stringify({ kind: "password", label: "x".repeat(500) }),
});
expect(res.status).toBe(413);
// The BOUNDARY, which nothing checked: a limit of N bytes has to mean N
// bytes are allowed. `>` accepts exactly N and `>=` rejects it, and
// sending 500 bytes at a 64-byte cap cannot tell the two apart — so the
// cap could quietly become off-by-one and only show up as a request
// that fails at a size the operator configured as fine.
const url = `http://${bound.host}:${bound.port}/api/v1/entry`;
const send = (body: string) =>
fetch(url, {
method: "POST",
headers: { authorization: `Bearer ${TOKEN}`, "content-type": "application/json" },
body,
});
// Exactly 64 bytes of valid JSON: accepted (whatever the route then
// makes of it — the point is that it is not refused for its size).
const padded = `{"kind":"password","label":"${"x".repeat(64 - 30)}"}`;
expect(padded.length).toBe(64);
expect((await send(padded)).status).not.toBe(413);
// One byte more: refused.
const overBy1 = `{"kind":"password","label":"${"x".repeat(64 - 29)}"}`;
expect(overBy1.length).toBe(65);
expect((await send(overBy1)).status).toBe(413);
} finally {
await small.close();
}
+64 -1
View File
@@ -34,7 +34,13 @@ import {
INDEX_FILENAME,
type ProgramIo,
} from "../src/index.js";
import { setFactor, tagForFactor, factorConfigured, partitionPath } from "../src/highRisk.js";
import {
setFactor,
tagForFactor,
factorConfigured,
partitionPath,
envelopePath,
} from "../src/highRisk.js";
import {
issueApproval,
listApprovals,
@@ -152,6 +158,25 @@ describe("the factor", () => {
expect(String((r.error as Error).message)).toContain("SEEDPASS_HIGH_RISK_FACTOR");
});
it("says NOT CONFIGURED rather than blaming the envelope", async () => {
// Without the existsSync guard, readFile's ENOENT is caught by the same
// handler that catches malformed JSON, so a profile that never set a
// factor is told its partition envelope is invalid. Both refuse, so only
// the reason is observable — and the reason is the whole difference
// between "set one up" and "yours is corrupt, restore a backup".
const fresh = await mkdtemp(join(tmpdir(), "seedpass-no-envelope-"));
await expect(tagForFactor(fresh, "anything")).rejects.toMatchObject({
reason: "high_risk_partition_not_configured",
});
// A file that exists but is not JSON is the case that reason belongs to.
await setFactor(fresh, "a-real-factor");
await writeFile(envelopePath(fresh), "{not json");
await expect(tagForFactor(fresh, "a-real-factor")).rejects.toMatchObject({
reason: "invalid_partition_envelope",
});
});
it("verifies correctly and rejects a wrong one", async () => {
expect(await tagForFactor(appDir, FACTOR)).toMatch(/^[0-9a-f]{64}$/);
await expect(tagForFactor(appDir, "wrong")).rejects.toMatchObject({
@@ -351,6 +376,44 @@ describe("approval gates", () => {
expect((await listApprovals(appDir)).find((a) => a.id === issued.id)!.uses_remaining).toBe(1);
});
it("honours resource scoping in both directions", async () => {
// Only the FAILING side of resource scoping was tested, and the check is
// `record !== "*" && record !== requested` — for a scoped record and a
// different resource both halves are true, so `&&` and `||` agree and a
// mutation there was invisible. These are the two cases where they
// differ, and they are the cases that make scoping useful rather than
// merely restrictive.
const scoped = await issueApproval(appDir, {
action: "export",
resource: "vault-a",
ttlSeconds: 300,
uses: 1,
});
// A scoped approval works for the resource it names.
expect(
await consumeApproval(appDir, {
approvalId: scoped.id,
action: "export",
resource: "vault-a",
}),
).toEqual({ ok: true, reason: "approval_consumed" });
// A wildcard approval works for any resource — that is what "*" is for.
const wildcard = await issueApproval(appDir, {
action: "export",
resource: "*",
ttlSeconds: 300,
uses: 1,
});
expect(
await consumeApproval(appDir, {
approvalId: wildcard.id,
action: "export",
resource: "some-specific-vault",
}),
).toEqual({ ok: true, reason: "approval_consumed" });
});
it("refuses an expired approval", async () => {
const issued = await issueApproval(appDir, {
action: "export",