[GH-ISSUE #3904] Admin plugin ac.newRole unexpectly requires all declared statements #27082

Closed
opened 2026-04-17 17:53:27 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @typed-sigterm on GitHub (Aug 10, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3904

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

const PermissionStatement = {
  'api-docs': ['read'],
  'tabletop': ['view', 'play', 'admin'],
  ...defaultStatements,
} as const;

const AccessController = createAccessControl(PermissionStatement);

const Role = {
  user: AccessController.newRole({}),
  admin: AccessController.newRole(...),
};

betterAuth({
  plugins: [
    admin({
      ac: AccessController,
      roles: Role,
      adminRoles: ['admin'],
    }),
  ],
})

Current vs. Expected behavior

Current:

src/utils/auth.ts(51,7): error TS2322: Type '{ user: { authorize<K_1 extends never>(request: K_1 extends infer T extends K ? { [key in T]?: Subset<never, { readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "update"]; readonly session: readonly ["list", "revoke", "delete"]; readonly 'api-docs': readonly [...]...' is not assignable to type '{ [x: string]: Role | undefined; }'.
  Property 'user' is incompatible with index signature.
    Type '{ authorize<K_1 extends never>(request: K_1 extends infer T extends K ? { [key in T]?: Subset<never, { readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "update"]; readonly session: readonly [...]; readonly 'api-docs': readonly [...]; readonly tabletop: readonly [...' is not assignable to type 'Role'.
      Types of property 'authorize' are incompatible.
        Type '<K_1 extends never>(request: K_1 extends infer T extends K ? { [key in T]?: Subset<never, { readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "update"]; readonly session: readonly [...]; readonly 'api-docs': readonly [...]; readonly tabletop: readonly [...]; }>[ke...' is not assignable to type '(request: any, connector?: "OR" | "AND" | undefined) => AuthorizeResponse'.
          Types of parameters 'request' and 'request' are incompatible.
            Type 'any' is not assignable to type 'never'.

Expected: fallback to [] if a key (e.g. user session) is not provided

What version of Better Auth are you using?

1.3.5-beta.5

Provide environment information

System:
    OS: Windows 11 10.0.26100
    CPU: (14) x64 Intel(R) Core(TM) Ultra 5 225H
    Memory: 12.43 GB / 31.50 GB
  Browsers:
    Edge: Chromium (138.0.3351.83)

Which area(s) are affected? (Select all that apply)

Types

Auth config (if applicable)


Additional context

No response

Originally created by @typed-sigterm on GitHub (Aug 10, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3904 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce ```ts const PermissionStatement = { 'api-docs': ['read'], 'tabletop': ['view', 'play', 'admin'], ...defaultStatements, } as const; const AccessController = createAccessControl(PermissionStatement); const Role = { user: AccessController.newRole({}), admin: AccessController.newRole(...), }; betterAuth({ plugins: [ admin({ ac: AccessController, roles: Role, adminRoles: ['admin'], }), ], }) ``` ### Current vs. Expected behavior Current: ``` src/utils/auth.ts(51,7): error TS2322: Type '{ user: { authorize<K_1 extends never>(request: K_1 extends infer T extends K ? { [key in T]?: Subset<never, { readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "update"]; readonly session: readonly ["list", "revoke", "delete"]; readonly 'api-docs': readonly [...]...' is not assignable to type '{ [x: string]: Role | undefined; }'. Property 'user' is incompatible with index signature. Type '{ authorize<K_1 extends never>(request: K_1 extends infer T extends K ? { [key in T]?: Subset<never, { readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "update"]; readonly session: readonly [...]; readonly 'api-docs': readonly [...]; readonly tabletop: readonly [...' is not assignable to type 'Role'. Types of property 'authorize' are incompatible. Type '<K_1 extends never>(request: K_1 extends infer T extends K ? { [key in T]?: Subset<never, { readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "update"]; readonly session: readonly [...]; readonly 'api-docs': readonly [...]; readonly tabletop: readonly [...]; }>[ke...' is not assignable to type '(request: any, connector?: "OR" | "AND" | undefined) => AuthorizeResponse'. Types of parameters 'request' and 'request' are incompatible. Type 'any' is not assignable to type 'never'. ``` Expected: fallback to `[]` if a key (e.g. `user` `session`) is not provided ### What version of Better Auth are you using? 1.3.5-beta.5 ### Provide environment information ```bash System: OS: Windows 11 10.0.26100 CPU: (14) x64 Intel(R) Core(TM) Ultra 5 225H Memory: 12.43 GB / 31.50 GB Browsers: Edge: Chromium (138.0.3351.83) ``` ### Which area(s) are affected? (Select all that apply) Types ### Auth config (if applicable) ```typescript ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-17 17:53:27 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Aug 10, 2025):

This TypeScript error happens because Better Auth's type system (and the admin plugin) expects all permission keys to be explicitly declared in each role, even if a role has no permissions for some keys. There is no built-in fallback to empty arrays for missing keys—if you omit a key, TypeScript's inference can break down, especially in strict mode, and you'll get type errors like the one you posted. This is reflected in Better Auth's own codebase and docs, where roles are always defined with all keys present, using [] for those with no permissions (example).

To fix this, always declare every permission key in your role definitions, like:

const Role = {
  user: AccessController.newRole({
    user: [],
    session: [],
    'api-docs': [],
    tabletop: [],
    // ...add all keys from PermissionStatement, [] for those with no permissions
  }),
  admin: AccessController.newRole({
    user: [...],
    session: [...],
    'api-docs': [...],
    tabletop: [...],
    // etc.
  }),
};

If you want to avoid manual repetition, you can write a helper to fill in missing keys with [] automatically. But the idiomatic and safest approach is to always specify all keys explicitly. There are no open issues or PRs to change this behavior—it's the intended pattern for type safety and plugin compatibility.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3172482156 --> @dosubot[bot] commented on GitHub (Aug 10, 2025): <!-- Answer --> This TypeScript error happens because Better Auth's type system (and the admin plugin) expects all permission keys to be explicitly declared in each role, even if a role has no permissions for some keys. There is no built-in fallback to empty arrays for missing keys—if you omit a key, TypeScript's inference can break down, especially in strict mode, and you'll get type errors like the one you posted. This is reflected in Better Auth's own codebase and docs, where roles are always defined with all keys present, using [] for those with no permissions ([example](https://github.com/better-auth/better-auth/blob/f281c92b9940fe6e81e05c07d24b73f2c2f8bfdd/packages/better-auth/src/plugins/admin/access/statement.ts)). To fix this, always declare every permission key in your role definitions, like: ```ts const Role = { user: AccessController.newRole({ user: [], session: [], 'api-docs': [], tabletop: [], // ...add all keys from PermissionStatement, [] for those with no permissions }), admin: AccessController.newRole({ user: [...], session: [...], 'api-docs': [...], tabletop: [...], // etc. }), }; ``` If you want to avoid manual repetition, you can write a helper to fill in missing keys with [] automatically. But the idiomatic and safest approach is to always specify all keys explicitly. There are no open issues or PRs to change this behavior—it's the intended pattern for type safety and plugin compatibility. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/9547602c-a332-4900-9b3a-428765806023?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/9547602c-a332-4900-9b3a-428765806023?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/9547602c-a332-4900-9b3a-428765806023?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/9547602c-a332-4900-9b3a-428765806023?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/9547602c-a332-4900-9b3a-428765806023?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/9547602c-a332-4900-9b3a-428765806023?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/9547602c-a332-4900-9b3a-428765806023?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3904)
Author
Owner

@Miunn commented on GitHub (Aug 10, 2025):

I believe that as you're re-defining the user role, you need to provide the default statements required by the admin plugin in that role. This can be done by expanding userAc.statements in your role definition

<!-- gh-comment-id:3172788686 --> @Miunn commented on GitHub (Aug 10, 2025): I believe that as you're re-defining the `user` role, you need to provide the default statements required by the admin plugin in that role. This can be done by expanding `userAc.statements` in your role definition
Author
Owner

@typed-sigterm commented on GitHub (Aug 11, 2025):

I mean treat missing statements as [], not only in this case

<!-- gh-comment-id:3174205259 --> @typed-sigterm commented on GitHub (Aug 11, 2025): I mean treat missing statements as `[]`, not only in this case
Author
Owner

@frectonz commented on GitHub (Aug 11, 2025):

It is much better to have each permission be explicitly set.

<!-- gh-comment-id:3176235247 --> @frectonz commented on GitHub (Aug 11, 2025): It is much better to have each permission be explicitly set.
Author
Owner

@typed-sigterm commented on GitHub (Aug 11, 2025):

I'm not sure about that. Deny-default is everywhere now, I believe omitting a statement is unlikely to cause confusion.

<!-- gh-comment-id:3176275628 --> @typed-sigterm commented on GitHub (Aug 11, 2025): I'm not sure about that. Deny-default is everywhere now, I believe omitting a statement is unlikely to cause confusion.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27082