From da85e64cfad012c046075cacd129d132d206a80b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 1 Aug 2026 22:45:04 +0200 Subject: [PATCH] Catch the Bearer resolver the guard's matcher walked past The Bearer guard demanded the literal `apiToken.findUnique(`. `lib/idempotency.ts` writes the same call fluent-style, with a newline between `prisma.apiToken` and `.findUnique({`, so the matcher never saw it. It has been resolving a raw Bearer value to a user id since the idempotency work landed, outside the frozen set, while the guard stayed green on five files. Tolerate whitespace around the member access and name the file with what it does: it decides which user's idempotency cell a replay lands in, and authorises nothing, because the handler still runs its own requireAuth. Add the non-zero count assertion the other guards now carry. Narrowing the regex back to its old form turns this test red, which is the proof the broadened matcher is what finds the file. The mint-site matcher in T3 was checked for the same defect and has no hidden members. --- .../bearer-scope-enforcement-guard.test.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/__tests__/bearer-scope-enforcement-guard.test.ts b/src/__tests__/bearer-scope-enforcement-guard.test.ts index 21c938fdc..b1c721cbb 100644 --- a/src/__tests__/bearer-scope-enforcement-guard.test.ts +++ b/src/__tests__/bearer-scope-enforcement-guard.test.ts @@ -64,16 +64,34 @@ describe("T1 — the Bearer resolution set is frozen", () => { // surface, which gates on BOTH `medication:ingest` and the per-medication // `medication::ingest` grant and never touches `requireAuth`. "app/api/ingest/medication/route.ts", + // `defaultUserIdResolver` — resolves the Bearer token to a user id so an + // idempotency key files under its owner. It authorises nothing: the + // handler still runs its own `requireAuth()`, and this resolver only + // decides which user's cache cell a replay lands in. Narrow in reach and + // deliberate, but it is a raw Bearer value becoming a user id, so it + // belongs on this list rather than beside it. + "lib/idempotency.ts", ].sort(); it("no file outside the allowlist resolves a Bearer token to a user", () => { // `resolveBearerToken` is the shared primitive; a `tokenHash` lookup is the // hand-rolled equivalent. Either one turns a bearer credential into an // identity, so both have to stay inside the allowlist. + // + // The whitespace classes around `.findUnique` are load-bearing. The + // original matcher demanded the literal `apiToken.findUnique(`, and + // `lib/idempotency.ts` writes the call fluent-style — `prisma.apiToken`, + // newline, `.findUnique({`. It resolved a Bearer token to a user id for + // three releases without this guard ever seeing it. A matcher that misses + // a real resolver is green because it matched nothing, not because + // nothing was there. const resolvers = filesMatching( - /resolveBearerToken|apiToken\.findUnique\([\s\S]*?where:\s*\{\s*tokenHash/, + /resolveBearerToken|apiToken\s*\.\s*findUnique\(\s*\{[\s\S]*?where:\s*\{\s*tokenHash/, ); + // Non-zero proof: an empty match set must fail rather than agree with an + // emptied allowlist. + expect(resolvers.length).toBeGreaterThan(0); expect(resolvers).toEqual(RESOLUTION_ALLOWLIST); });