@simple-auth-kit/cli 1.4.3 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/lib/copy.ts +1 -1
  2. package/package.json +1 -1
  3. package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  4. package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
  5. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  6. package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
  7. package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  8. package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  9. package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
  10. package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
  11. package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
  12. package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
  13. package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  14. package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
  15. package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
  16. package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
  17. package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
  18. package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
  19. package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  20. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
  21. package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
  22. package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
  23. package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
  24. package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
  25. package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
  26. package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
  27. package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  28. package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
  29. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  30. package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
  31. package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  32. package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  33. package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  34. package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
  35. package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
  36. package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +56 -21
  37. package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  38. package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +66 -18
  39. package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  40. package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
  41. package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
  42. package/registry/combos/express-prisma/variants/base/src/seed.ts +77 -19
  43. package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
  44. package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +80 -25
  45. package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  46. package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
  47. package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +72 -20
  48. package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  49. package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
  50. package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
  51. package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
  52. package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
  53. package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +108 -28
  54. package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
  55. package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  56. package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  57. package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  58. package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  59. package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
  60. package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
  61. package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  62. package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  63. package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
  64. package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
  65. package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
  66. package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
  67. package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
  68. package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  69. package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
  70. package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
  71. package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
  72. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
  73. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
  74. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  75. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  76. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
  77. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
  78. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
  79. package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
  80. package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  81. package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  82. package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  83. package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  84. package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
  85. package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
  86. package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  87. package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  88. package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  89. package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
  90. package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
  91. package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +58 -27
  92. package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
  93. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
  94. package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  95. package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +97 -26
  96. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  97. package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
  98. package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
  99. package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
  100. package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
  101. package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +77 -19
  102. package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
  103. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +54 -26
  104. package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
  105. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  106. package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  107. package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +103 -28
  108. package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  109. package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
  110. package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
  111. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
  112. package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
  113. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
  114. package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +108 -28
  115. package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
  116. package/registry.json +4 -4
  117. package/simple-auth-kit.ts +42 -11
  118. package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
  119. package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
  120. package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
  121. package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
  122. package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
  123. package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
  124. package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
  125. package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
  126. package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
  127. package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
  128. package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
  129. package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
  130. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
  131. package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
  132. package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
  133. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
  134. package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
@@ -1,10 +1,15 @@
1
1
  import { resolvePermissions } from "@/lib/auth/core/rbac.js";
2
- import { Prisma, PrismaClient } from "../generated/prisma/client.js";
3
- import type { AuthzContext } from "./authz.middleware.js";
4
- import { HttpError } from "./http-error.js";
5
- import { buildPageMeta, normalizeLimit, normalizePage, type Paginated } from "./pagination.js";
6
- import { PermissionCache } from "./permission-cache.js";
7
- import { toId, toIdOrNull } from "./id.helper.js";
2
+ import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
3
+ import type { AuthzContext } from "../middleware/authz.middleware.js";
4
+ import { HttpError } from "../http-error.js";
5
+ import {
6
+ buildPageMeta,
7
+ normalizeLimit,
8
+ normalizePage,
9
+ type Paginated,
10
+ } from "../pagination.js";
11
+ import { PermissionCache } from "../permission-cache.js";
12
+ import { toId, toIdOrNull } from "../id.helper.js";
8
13
 
9
14
  export interface MemberSummary {
10
15
  memberId: string;
@@ -36,10 +41,15 @@ export interface MemberListFilter {
36
41
 
37
42
  export type MemberListResult = Paginated<MemberSummary>;
38
43
 
39
- const MEMBER_INCLUDE = { user: true, roles: { select: { role: { select: { slug: true } } } } } as const;
44
+ const MEMBER_INCLUDE = {
45
+ user: true,
46
+ roles: { select: { role: { select: { slug: true } } } },
47
+ } as const;
40
48
 
41
49
  /** A `WorkspaceMember` row as `findUnique`/`findMany` return it, joined with its user and roles. */
42
- export type MemberRow = Prisma.WorkspaceMemberGetPayload<{ include: typeof MEMBER_INCLUDE }>;
50
+ export type MemberRow = Prisma.WorkspaceMemberGetPayload<{
51
+ include: typeof MEMBER_INCLUDE;
52
+ }>;
43
53
 
44
54
  /** Shapes a raw row into the wire DTO. Exported so `listMembers`'s collection can be shaped by the caller. */
45
55
  export function toMemberSummary(row: MemberRow): MemberSummary {
@@ -111,7 +121,14 @@ const PERMISSION_SELECT = {
111
121
  isActive: true,
112
122
  } as const;
113
123
 
114
- const ROLE_SELECT = { id: true, slug: true, name: true, displayName: true, isDefault: true, isActive: true } as const;
124
+ const ROLE_SELECT = {
125
+ id: true,
126
+ slug: true,
127
+ name: true,
128
+ displayName: true,
129
+ isDefault: true,
130
+ isActive: true,
131
+ } as const;
115
132
 
116
133
  /**
117
134
  * The cache subject in this variant: authorization belongs to a membership, so the key has to be
@@ -122,7 +139,8 @@ const ROLE_SELECT = { id: true, slug: true, name: true, displayName: true, isDef
122
139
  * between the two one-directional — the guard needs this repository at runtime, so the repository
123
140
  * must not need the guard.
124
141
  */
125
- export const memberCacheKey = (userId: string, workspaceId: string) => `${userId}:${workspaceId}`;
142
+ export const memberCacheKey = (userId: string, workspaceId: string) =>
143
+ `${userId}:${workspaceId}`;
126
144
 
127
145
  export class RbacRepository {
128
146
  constructor(
@@ -145,8 +163,15 @@ export class RbacRepository {
145
163
  // Takes bigint directly: its one caller (setMemberRoles) already has the membership's id in
146
164
  // hand from its own Prisma lookup, so there's nothing to parse here.
147
165
  private async invalidateMemberById(memberId: bigint): Promise<void> {
148
- const member = await this.prisma.workspaceMember.findUnique({ where: { id: memberId }, select: { userId: true, workspaceId: true } });
149
- if (member) await this.invalidateMember(member.userId.toString(), member.workspaceId.toString());
166
+ const member = await this.prisma.workspaceMember.findUnique({
167
+ where: { id: memberId },
168
+ select: { userId: true, workspaceId: true },
169
+ });
170
+ if (member)
171
+ await this.invalidateMember(
172
+ member.userId.toString(),
173
+ member.workspaceId.toString(),
174
+ );
150
175
  }
151
176
 
152
177
  /**
@@ -164,7 +189,10 @@ export class RbacRepository {
164
189
  * `isActive: false` on a role or a permission takes it out of the answer, which is what makes
165
190
  * deactivating either one enforcement-changing without unpicking any grant.
166
191
  */
167
- async resolveAuthzContext(userId: string, workspaceId: string): Promise<AuthzContext | null> {
192
+ async resolveAuthzContext(
193
+ userId: string,
194
+ workspaceId: string,
195
+ ): Promise<AuthzContext | null> {
168
196
  // workspaceId arrives straight from the untrusted `X-Workspace-Id` header — a value that
169
197
  // isn't a valid bigint is exactly as "not a member" as one that parses but names no
170
198
  // membership, so it must not throw and 500 the request.
@@ -178,7 +206,10 @@ export class RbacRepository {
178
206
  }
179
207
 
180
208
  const member = await this.prisma.workspaceMember.findUnique({
181
- where: { userId_workspaceId: { userId: userIdBig, workspaceId: workspaceIdBig }, user: { isDeleted: false } },
209
+ where: {
210
+ userId_workspaceId: { userId: userIdBig, workspaceId: workspaceIdBig },
211
+ user: { isDeleted: false },
212
+ },
182
213
  select: {
183
214
  id: true,
184
215
  roles: {
@@ -187,18 +218,28 @@ export class RbacRepository {
187
218
  role: {
188
219
  select: {
189
220
  slug: true,
190
- permissions: { where: { permission: { isActive: true, isDeleted: false } }, select: { permission: { select: { slug: true } } } },
221
+ permissions: {
222
+ where: { permission: { isActive: true, isDeleted: false } },
223
+ select: { permission: { select: { slug: true } } },
224
+ },
191
225
  },
192
226
  },
193
227
  },
194
228
  },
195
- permissions: { where: { permission: { isActive: true, isDeleted: false } }, select: { permission: { select: { slug: true } } } },
229
+ permissions: {
230
+ where: { permission: { isActive: true, isDeleted: false } },
231
+ select: { permission: { select: { slug: true } } },
232
+ },
196
233
  },
197
234
  });
198
235
  if (!member) return null;
199
236
 
200
- const rolePermissions = member.roles.flatMap((roleMember) => roleMember.role.permissions.map((pr) => pr.permission.slug));
201
- const directPermissions = member.permissions.map((pm) => pm.permission.slug);
237
+ const rolePermissions = member.roles.flatMap((roleMember) =>
238
+ roleMember.role.permissions.map((pr) => pr.permission.slug),
239
+ );
240
+ const directPermissions = member.permissions.map(
241
+ (pm) => pm.permission.slug,
242
+ );
202
243
 
203
244
  return {
204
245
  workspaceId,
@@ -214,12 +255,21 @@ export class RbacRepository {
214
255
  * caller feeds the id straight into another Prisma call, and the one external caller
215
256
  * (AuthService) discards the result.
216
257
  */
217
- async requireMember(workspaceId: string, userId: string): Promise<{ id: bigint }> {
258
+ async requireMember(
259
+ workspaceId: string,
260
+ userId: string,
261
+ ): Promise<{ id: bigint }> {
218
262
  const member = await this.prisma.workspaceMember.findUnique({
219
- where: { userId_workspaceId: { userId: toId(userId), workspaceId: toId(workspaceId) } },
263
+ where: {
264
+ userId_workspaceId: {
265
+ userId: toId(userId),
266
+ workspaceId: toId(workspaceId),
267
+ },
268
+ },
220
269
  select: { id: true },
221
270
  });
222
- if (!member) throw new HttpError(404, "user is not a member of this workspace");
271
+ if (!member)
272
+ throw new HttpError(404, "user is not a member of this workspace");
223
273
  return member;
224
274
  }
225
275
 
@@ -227,13 +277,21 @@ export class RbacRepository {
227
277
  * Newest-first, page-paginated; `search` matches an email substring. Returns raw rows —
228
278
  * shaping the collection is the caller's job, see `toMemberSummary`.
229
279
  */
230
- async listMembers(workspaceId: string, filter: MemberListFilter = {}): Promise<Paginated<MemberRow>> {
280
+ async listMembers(
281
+ workspaceId: string,
282
+ filter: MemberListFilter = {},
283
+ ): Promise<Paginated<MemberRow>> {
231
284
  const page = normalizePage(filter.page);
232
285
  const limit = normalizeLimit(filter.limit);
233
286
  const workspaceIdBig = toId(workspaceId);
234
287
  const where = {
235
288
  workspaceId: workspaceIdBig,
236
- user: { isDeleted: false, email: filter.search ? { contains: filter.search, mode: "insensitive" as const } : undefined },
289
+ user: {
290
+ isDeleted: false,
291
+ email: filter.search
292
+ ? { contains: filter.search, mode: "insensitive" as const }
293
+ : undefined,
294
+ },
237
295
  };
238
296
 
239
297
  const [rows, total] = await this.prisma.$transaction([
@@ -268,11 +326,21 @@ export class RbacRepository {
268
326
  async updateMember(
269
327
  workspaceId: string,
270
328
  userId: string,
271
- input: { firstName?: string | null; lastName?: string | null; displayName?: string | null; phone?: string | null; username?: string | null; photo?: string | null },
329
+ input: {
330
+ firstName?: string | null;
331
+ lastName?: string | null;
332
+ displayName?: string | null;
333
+ phone?: string | null;
334
+ username?: string | null;
335
+ photo?: string | null;
336
+ },
272
337
  actorUserId: string | null,
273
338
  ): Promise<MemberSummary> {
274
339
  await this.requireMember(workspaceId, userId);
275
- await this.prisma.user.update({ where: { id: toId(userId) }, data: { ...input, updatedBy: toIdOrNull(actorUserId) } });
340
+ await this.prisma.user.update({
341
+ where: { id: toId(userId) },
342
+ data: { ...input, updatedBy: toIdOrNull(actorUserId) },
343
+ });
276
344
  return this.getMember(workspaceId, userId);
277
345
  }
278
346
 
@@ -280,11 +348,21 @@ export class RbacRepository {
280
348
  // pattern, and `block`'s existing precedent of a workspace admin taking a global action on a
281
349
  // member's account — this disables the account across every workspace it belongs to, not just
282
350
  // this one, which is why it's gated the same way `block` is.
283
- async deleteMember(workspaceId: string, userId: string, actorUserId: string | null, reason?: string): Promise<void> {
351
+ async deleteMember(
352
+ workspaceId: string,
353
+ userId: string,
354
+ actorUserId: string | null,
355
+ reason?: string,
356
+ ): Promise<void> {
284
357
  await this.requireMember(workspaceId, userId);
285
358
  await this.prisma.user.update({
286
359
  where: { id: toId(userId) },
287
- data: { isDeleted: true, deletedAt: new Date(), deletedBy: toIdOrNull(actorUserId), deletedReason: reason ?? null },
360
+ data: {
361
+ isDeleted: true,
362
+ deletedAt: new Date(),
363
+ deletedBy: toIdOrNull(actorUserId),
364
+ deletedReason: reason ?? null,
365
+ },
288
366
  });
289
367
  await this.invalidateMember(userId, workspaceId);
290
368
  }
@@ -292,13 +370,22 @@ export class RbacRepository {
292
370
  async updateRole(
293
371
  workspaceId: string,
294
372
  roleId: string,
295
- input: { name?: string; displayName?: string; description?: string | null; isActive?: boolean },
373
+ input: {
374
+ name?: string;
375
+ displayName?: string;
376
+ description?: string | null;
377
+ isActive?: boolean;
378
+ },
296
379
  actorUserId: string | null,
297
380
  ): Promise<RoleSummary> {
298
381
  const workspaceIdBig = toId(workspaceId);
299
382
  const roleIdBig = toId(roleId);
300
- const existing = await this.prisma.role.findUnique({ where: { id: roleIdBig, isDeleted: false }, select: { id: true, workspaceId: true } });
301
- if (!existing || existing.workspaceId !== workspaceIdBig) throw new HttpError(404, "role not found in this workspace");
383
+ const existing = await this.prisma.role.findUnique({
384
+ where: { id: roleIdBig, isDeleted: false },
385
+ select: { id: true, workspaceId: true },
386
+ });
387
+ if (!existing || existing.workspaceId !== workspaceIdBig)
388
+ throw new HttpError(404, "role not found in this workspace");
302
389
  const role = await this.prisma.role.update({
303
390
  where: { id: roleIdBig },
304
391
  data: { ...input, updatedBy: toIdOrNull(actorUserId) },
@@ -310,14 +397,28 @@ export class RbacRepository {
310
397
 
311
398
  // Soft-delete. `role_member`/`permission_role` rows pointing at it are left in place — the
312
399
  // role simply stops being resolved by `resolveAuthzContext`/`listRoles`.
313
- async deleteRole(workspaceId: string, roleId: string, actorUserId: string | null, reason?: string): Promise<void> {
400
+ async deleteRole(
401
+ workspaceId: string,
402
+ roleId: string,
403
+ actorUserId: string | null,
404
+ reason?: string,
405
+ ): Promise<void> {
314
406
  const workspaceIdBig = toId(workspaceId);
315
407
  const roleIdBig = toId(roleId);
316
- const existing = await this.prisma.role.findUnique({ where: { id: roleIdBig, isDeleted: false }, select: { id: true, workspaceId: true } });
317
- if (!existing || existing.workspaceId !== workspaceIdBig) throw new HttpError(404, "role not found in this workspace");
408
+ const existing = await this.prisma.role.findUnique({
409
+ where: { id: roleIdBig, isDeleted: false },
410
+ select: { id: true, workspaceId: true },
411
+ });
412
+ if (!existing || existing.workspaceId !== workspaceIdBig)
413
+ throw new HttpError(404, "role not found in this workspace");
318
414
  await this.prisma.role.update({
319
415
  where: { id: roleIdBig },
320
- data: { isDeleted: true, deletedAt: new Date(), deletedBy: toIdOrNull(actorUserId), deletedReason: reason ?? null },
416
+ data: {
417
+ isDeleted: true,
418
+ deletedAt: new Date(),
419
+ deletedBy: toIdOrNull(actorUserId),
420
+ deletedReason: reason ?? null,
421
+ },
321
422
  });
322
423
  await this.cache.invalidatePolicy();
323
424
  }
@@ -333,7 +434,12 @@ export class RbacRepository {
333
434
  const rows = await this.prisma.permission.findMany({
334
435
  where: { isDeleted: false },
335
436
  select: PERMISSION_SELECT,
336
- orderBy: [{ groupOrder: "asc" }, { group: "asc" }, { order: "asc" }, { slug: "asc" }],
437
+ orderBy: [
438
+ { groupOrder: "asc" },
439
+ { group: "asc" },
440
+ { order: "asc" },
441
+ { slug: "asc" },
442
+ ],
337
443
  });
338
444
  return rows.map((row) => ({ ...row, id: row.id.toString() }));
339
445
  }
@@ -347,7 +453,10 @@ export class RbacRepository {
347
453
  * Upserted on `slug`, which stays the stable identifier grants and revocations use, so renaming
348
454
  * `name`/`displayName` never breaks a grant.
349
455
  */
350
- async upsertPermission(input: PermissionInput, actorUserId: string | null): Promise<PermissionSummary> {
456
+ async upsertPermission(
457
+ input: PermissionInput,
458
+ actorUserId: string | null,
459
+ ): Promise<PermissionSummary> {
351
460
  const shared = {
352
461
  name: input.name,
353
462
  displayName: input.displayName,
@@ -389,7 +498,12 @@ export class RbacRepository {
389
498
 
390
499
  async createRole(
391
500
  workspaceId: string,
392
- input: { slug: string; name?: string; displayName?: string; description?: string | null },
501
+ input: {
502
+ slug: string;
503
+ name?: string;
504
+ displayName?: string;
505
+ description?: string | null;
506
+ },
393
507
  actorUserId: string | null,
394
508
  ): Promise<RoleSummary> {
395
509
  const role = await this.prisma.role.create({
@@ -413,22 +527,37 @@ export class RbacRepository {
413
527
  * be granted, and it opens nothing until a route names it — and no route shipped by this
414
528
  * library can name it, because `@CheckAbility` only accepts slugs from `rbac.defaults.ts`.
415
529
  */
416
- async attachPermissionToRole(workspaceId: string, roleId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
530
+ async attachPermissionToRole(
531
+ workspaceId: string,
532
+ roleId: string,
533
+ permissionSlug: string,
534
+ actorUserId: string | null,
535
+ ): Promise<void> {
417
536
  const workspaceIdBig = toId(workspaceId);
418
537
  const roleIdBig = toId(roleId);
419
- const role = await this.prisma.role.findUnique({ where: { id: roleIdBig, isDeleted: false }, select: { id: true, workspaceId: true } });
420
- if (!role || role.workspaceId !== workspaceIdBig) throw new HttpError(404, "role not found in this workspace");
538
+ const role = await this.prisma.role.findUnique({
539
+ where: { id: roleIdBig, isDeleted: false },
540
+ select: { id: true, workspaceId: true },
541
+ });
542
+ if (!role || role.workspaceId !== workspaceIdBig)
543
+ throw new HttpError(404, "role not found in this workspace");
421
544
 
422
545
  const permission = await this.ensurePermission(permissionSlug, actorUserId);
423
546
  await this.prisma.permissionRole.upsert({
424
- where: { permissionId_roleId: { permissionId: permission.id, roleId: roleIdBig } },
547
+ where: {
548
+ permissionId_roleId: { permissionId: permission.id, roleId: roleIdBig },
549
+ },
425
550
  create: { permissionId: permission.id, roleId: roleIdBig },
426
551
  update: {},
427
552
  });
428
553
  await this.cache.invalidatePolicy(); // every membership holding this role is affected
429
554
  }
430
555
 
431
- async assignRoleToMember(workspaceId: string, userId: string, roleSlug: string): Promise<void> {
556
+ async assignRoleToMember(
557
+ workspaceId: string,
558
+ userId: string,
559
+ roleSlug: string,
560
+ ): Promise<void> {
432
561
  const role = await this.requireRole(workspaceId, roleSlug);
433
562
  const member = await this.requireMember(workspaceId, userId);
434
563
  await this.prisma.roleMember.upsert({
@@ -439,32 +568,64 @@ export class RbacRepository {
439
568
  await this.invalidateMember(userId, workspaceId);
440
569
  }
441
570
 
442
- async revokeRoleFromMember(workspaceId: string, userId: string, roleSlug: string): Promise<void> {
571
+ async revokeRoleFromMember(
572
+ workspaceId: string,
573
+ userId: string,
574
+ roleSlug: string,
575
+ ): Promise<void> {
443
576
  const member = await this.requireMember(workspaceId, userId);
444
- const role = await this.prisma.role.findUnique({ where: { workspaceId_slug: { workspaceId: toId(workspaceId), slug: roleSlug } }, select: { id: true } });
577
+ const role = await this.prisma.role.findUnique({
578
+ where: {
579
+ workspaceId_slug: { workspaceId: toId(workspaceId), slug: roleSlug },
580
+ },
581
+ select: { id: true },
582
+ });
445
583
  if (!role) return;
446
- await this.prisma.roleMember.deleteMany({ where: { memberId: member.id, roleId: role.id } });
584
+ await this.prisma.roleMember.deleteMany({
585
+ where: { memberId: member.id, roleId: role.id },
586
+ });
447
587
  await this.invalidateMember(userId, workspaceId);
448
588
  }
449
589
 
450
590
  /** Replaces the whole set — the "set a member's roles" operation, as opposed to assign/revoke one at a time. */
451
- async setMemberRoles(workspaceId: string, memberId: string, roleSlugs: string[]): Promise<{ memberId: string; roles: string[] }> {
591
+ async setMemberRoles(
592
+ workspaceId: string,
593
+ memberId: string,
594
+ roleSlugs: string[],
595
+ ): Promise<{ memberId: string; roles: string[] }> {
452
596
  const workspaceIdBig = toId(workspaceId);
453
597
  const memberIdBig = toId(memberId);
454
- const member = await this.prisma.workspaceMember.findUnique({ where: { id: memberIdBig }, select: { id: true, workspaceId: true } });
455
- if (!member || member.workspaceId !== workspaceIdBig) throw new HttpError(404, "member not found in this workspace");
598
+ const member = await this.prisma.workspaceMember.findUnique({
599
+ where: { id: memberIdBig },
600
+ select: { id: true, workspaceId: true },
601
+ });
602
+ if (!member || member.workspaceId !== workspaceIdBig)
603
+ throw new HttpError(404, "member not found in this workspace");
456
604
 
457
605
  const roles = await this.prisma.role.findMany({
458
- where: { workspaceId: workspaceIdBig, slug: { in: roleSlugs }, isDeleted: false },
606
+ where: {
607
+ workspaceId: workspaceIdBig,
608
+ slug: { in: roleSlugs },
609
+ isDeleted: false,
610
+ },
459
611
  select: { id: true, slug: true },
460
612
  });
461
- const unknown = roleSlugs.filter((slug) => !roles.some((role) => role.slug === slug));
462
- if (unknown.length) throw new HttpError(404, `role(s) not defined in this workspace: ${unknown.join(", ")}`);
613
+ const unknown = roleSlugs.filter(
614
+ (slug) => !roles.some((role) => role.slug === slug),
615
+ );
616
+ if (unknown.length)
617
+ throw new HttpError(
618
+ 404,
619
+ `role(s) not defined in this workspace: ${unknown.join(", ")}`,
620
+ );
463
621
 
464
622
  // Replace, in one transaction: a member must never be briefly role-less to a concurrent request.
465
623
  await this.prisma.$transaction([
466
624
  this.prisma.roleMember.deleteMany({ where: { memberId: memberIdBig } }),
467
- this.prisma.roleMember.createMany({ data: roles.map((role) => ({ memberId: memberIdBig, roleId: role.id })), skipDuplicates: true }),
625
+ this.prisma.roleMember.createMany({
626
+ data: roles.map((role) => ({ memberId: memberIdBig, roleId: role.id })),
627
+ skipDuplicates: true,
628
+ }),
468
629
  ]);
469
630
  await this.invalidateMemberById(memberIdBig);
470
631
  return { memberId, roles: roles.map((role) => role.slug).sort() };
@@ -477,36 +638,70 @@ export class RbacRepository {
477
638
  */
478
639
  async defaultRoleIdsFor(workspaceId: string): Promise<bigint[]> {
479
640
  const roles = await this.prisma.role.findMany({
480
- where: { workspaceId: toId(workspaceId), isDefault: true, isActive: true, isDeleted: false },
641
+ where: {
642
+ workspaceId: toId(workspaceId),
643
+ isDefault: true,
644
+ isActive: true,
645
+ isDeleted: false,
646
+ },
481
647
  select: { id: true },
482
648
  });
483
649
  return roles.map((role) => role.id);
484
650
  }
485
651
 
486
- async grantPermissionToMember(workspaceId: string, userId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
652
+ async grantPermissionToMember(
653
+ workspaceId: string,
654
+ userId: string,
655
+ permissionSlug: string,
656
+ actorUserId: string | null,
657
+ ): Promise<void> {
487
658
  const member = await this.requireMember(workspaceId, userId);
488
659
  const permission = await this.ensurePermission(permissionSlug, actorUserId);
489
660
  await this.prisma.permissionMember.upsert({
490
- where: { memberId_permissionId: { memberId: member.id, permissionId: permission.id } },
661
+ where: {
662
+ memberId_permissionId: {
663
+ memberId: member.id,
664
+ permissionId: permission.id,
665
+ },
666
+ },
491
667
  create: { memberId: member.id, permissionId: permission.id },
492
668
  update: {},
493
669
  });
494
670
  await this.invalidateMember(userId, workspaceId);
495
671
  }
496
672
 
497
- async revokePermissionFromMember(workspaceId: string, userId: string, permissionSlug: string): Promise<void> {
673
+ async revokePermissionFromMember(
674
+ workspaceId: string,
675
+ userId: string,
676
+ permissionSlug: string,
677
+ ): Promise<void> {
498
678
  const member = await this.requireMember(workspaceId, userId);
499
- const permission = await this.prisma.permission.findUnique({ where: { slug: permissionSlug }, select: { id: true } });
679
+ const permission = await this.prisma.permission.findUnique({
680
+ where: { slug: permissionSlug },
681
+ select: { id: true },
682
+ });
500
683
  if (!permission) return;
501
- await this.prisma.permissionMember.deleteMany({ where: { memberId: member.id, permissionId: permission.id } });
684
+ await this.prisma.permissionMember.deleteMany({
685
+ where: { memberId: member.id, permissionId: permission.id },
686
+ });
502
687
  await this.invalidateMember(userId, workspaceId);
503
688
  }
504
689
 
505
690
  // Returns bigint directly: its one caller (assignRoleToMember) feeds the id straight into
506
691
  // another Prisma call.
507
- private async requireRole(workspaceId: string, slug: string): Promise<{ id: bigint }> {
508
- const role = await this.prisma.role.findUnique({ where: { workspaceId_slug: { workspaceId: toId(workspaceId), slug }, isDeleted: false }, select: { id: true } });
509
- if (!role) throw new HttpError(404, `role "${slug}" not found in this workspace`);
692
+ private async requireRole(
693
+ workspaceId: string,
694
+ slug: string,
695
+ ): Promise<{ id: bigint }> {
696
+ const role = await this.prisma.role.findUnique({
697
+ where: {
698
+ workspaceId_slug: { workspaceId: toId(workspaceId), slug },
699
+ isDeleted: false,
700
+ },
701
+ select: { id: true },
702
+ });
703
+ if (!role)
704
+ throw new HttpError(404, `role "${slug}" not found in this workspace`);
510
705
  return role;
511
706
  }
512
707
 
@@ -514,12 +709,25 @@ export class RbacRepository {
514
709
  * Creates the row for a slug granted before anyone defined it; leaves an existing definition
515
710
  * alone. Returns bigint directly: both callers feed the id straight into another Prisma call.
516
711
  */
517
- private async ensurePermission(slug: string, actorUserId: string | null): Promise<{ id: bigint }> {
518
- const existing = await this.prisma.permission.findUnique({ where: { slug }, select: { id: true } });
712
+ private async ensurePermission(
713
+ slug: string,
714
+ actorUserId: string | null,
715
+ ): Promise<{ id: bigint }> {
716
+ const existing = await this.prisma.permission.findUnique({
717
+ where: { slug },
718
+ select: { id: true },
719
+ });
519
720
  return (
520
721
  existing ??
521
722
  (await this.prisma.permission.create({
522
- data: { slug, name: slug, displayName: slug, group: "Custom", createdBy: toIdOrNull(actorUserId), updatedBy: toIdOrNull(actorUserId) },
723
+ data: {
724
+ slug,
725
+ name: slug,
726
+ displayName: slug,
727
+ group: "Custom",
728
+ createdBy: toIdOrNull(actorUserId),
729
+ updatedBy: toIdOrNull(actorUserId),
730
+ },
523
731
  select: { id: true },
524
732
  }))
525
733
  );