Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/api/src/auth/permission.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,17 @@ describe('PermissionGuard', () => {

const result = await guard.canActivate(context);
expect(result).toBe(true);
// The body MUST include `permission: undefined` explicitly — zod 4 in
// better-auth's hasPermission schema requires both discriminated-union
// keys to be present, and treats omitted-vs-undefined as different.
// Without the explicit undefined, every cookie-authenticated request
// 403s with "Unable to verify permissions". Pin this in the test so
// a future refactor that drops the field gets caught.
expect(mockHasPermission).toHaveBeenCalledWith({
headers: expect.any(Headers),
body: {
permissions: { control: ['delete'] },
permission: undefined,
},
});
});
Expand Down
12 changes: 11 additions & 1 deletion apps/api/src/auth/permission.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,19 @@ export class PermissionGuard implements CanActivate {
return false;
}

// better-auth 1.4.x's `hasPermission` body schema is a discriminated
// union that requires BOTH keys present, with the unused side set to
// an explicit `undefined`. zod 4 (which better-auth's plugin uses
// internally) rejects "key is absent" — only "key is undefined" passes
// the `z.undefined()` branch. Without the explicit `permission: undefined`,
// the schema rejects every request with `[body] Invalid input`, the
// catch in canActivate turns that into a generic "Unable to verify
// permissions" 403, and EVERY cookie-authenticated request returns 403.
// Reproduced repo-side via `bun run zod-repro.mjs`. Discovered on
// ENG-221 and the same fix applies here.
const result = await auth.api.hasPermission({
headers,
body: { permissions },
body: { permissions, permission: undefined },
});

return result.success === true;
Expand Down
Loading