diff --git a/js/packages/cli/test/api.test.ts b/js/packages/cli/test/api.test.ts index f7bb5ed7..b2ba7d5c 100644 --- a/js/packages/cli/test/api.test.ts +++ b/js/packages/cli/test/api.test.ts @@ -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(); } diff --git a/js/packages/cli/test/highRisk.test.ts b/js/packages/cli/test/highRisk.test.ts index 078b44bf..f27a773c 100644 --- a/js/packages/cli/test/highRisk.test.ts +++ b/js/packages/cli/test/highRisk.test.ts @@ -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",