@simple-auth-kit/cli 1.4.2 → 1.5.0

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 (133) hide show
  1. package/package.json +1 -1
  2. package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  3. package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
  4. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  5. package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
  6. package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  7. package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  8. package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
  9. package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
  10. package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
  11. package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
  12. package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  13. package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
  14. package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
  15. package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
  16. package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
  17. package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
  18. package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  19. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
  20. package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
  21. package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
  22. package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
  23. package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
  24. package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
  25. package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
  26. package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  27. package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
  28. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  29. package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
  30. package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  31. package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  32. package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  33. package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
  34. package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
  35. package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +57 -22
  36. package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  37. package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +67 -19
  38. package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  39. package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
  40. package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
  41. package/registry/combos/express-prisma/variants/base/src/seed.ts +78 -20
  42. package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
  43. package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +81 -26
  44. package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  45. package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
  46. package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +73 -21
  47. package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  48. package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
  49. package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
  50. package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
  51. package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
  52. package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +109 -29
  53. package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
  54. package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  55. package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  56. package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  57. package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  58. package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
  59. package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
  60. package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  61. package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  62. package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
  63. package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
  64. package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
  65. package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
  66. package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
  67. package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  68. package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
  69. package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
  70. package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
  71. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
  72. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
  73. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  74. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  75. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
  76. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
  77. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
  78. package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
  79. package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  80. package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  81. package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  82. package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  83. package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
  84. package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
  85. package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  86. package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  87. package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  88. package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
  89. package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
  90. package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +59 -28
  91. package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
  92. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
  93. package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  94. package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +98 -27
  95. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  96. package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
  97. package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
  98. package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
  99. package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
  100. package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +78 -20
  101. package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
  102. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +55 -27
  103. package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
  104. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  105. package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  106. package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +104 -29
  107. package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  108. package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
  109. package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
  110. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
  111. package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
  112. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
  113. package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +109 -29
  114. package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
  115. package/registry.json +6 -4
  116. package/simple-auth-kit.ts +582 -124
  117. package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
  118. package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
  119. package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
  120. package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
  121. package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
  122. package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
  123. package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
  124. package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
  125. package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
  126. package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
  127. package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
  128. package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
  129. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
  130. package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
  131. package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
  132. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
  133. package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
@@ -1,12 +1,34 @@
1
1
  import { Inject, Injectable, NotFoundException } from "@nestjs/common";
2
- import { and, asc, count, desc, eq, ilike, inArray, type SQL } from "drizzle-orm";
2
+ import {
3
+ and,
4
+ asc,
5
+ count,
6
+ desc,
7
+ eq,
8
+ ilike,
9
+ inArray,
10
+ type SQL,
11
+ } from "drizzle-orm";
3
12
  import { resolvePermissions } from "@/lib/auth/core/rbac.js";
4
- import type { AuthzContext } from "./authz.guard.js";
5
- import { DRIZZLE_DB, type Database } from "./db.js";
6
- import { buildPageMeta, normalizeLimit, normalizePage, type Paginated } from "./pagination.js";
7
- import { PermissionCache } from "./permission-cache.js";
8
- import { permissionMember, permissionRole, permissions, roleMember, roles, users, workspaceMembers } from "./schema.js";
9
- import { toId, toIdOrNull } from "./id.helper.js";
13
+ import type { AuthzContext } from "../guards/authz.guard.js";
14
+ import { DRIZZLE_DB, type Database } from "../db.js";
15
+ import {
16
+ buildPageMeta,
17
+ normalizeLimit,
18
+ normalizePage,
19
+ type Paginated,
20
+ } from "../pagination.js";
21
+ import { PermissionCache } from "../permission-cache.js";
22
+ import {
23
+ permissionMember,
24
+ permissionRole,
25
+ permissions,
26
+ roleMember,
27
+ roles,
28
+ users,
29
+ workspaceMembers,
30
+ } from "../schema.js";
31
+ import { toId, toIdOrNull } from "../id.helper.js";
10
32
 
11
33
  export interface MemberSummary {
12
34
  memberId: string;
@@ -196,9 +218,15 @@ interface RoleRow {
196
218
  isActive: boolean;
197
219
  }
198
220
 
199
- const asPermissionSummary = (row: PermissionRow): PermissionSummary => ({ ...row, id: row.id.toString() });
221
+ const asPermissionSummary = (row: PermissionRow): PermissionSummary => ({
222
+ ...row,
223
+ id: row.id.toString(),
224
+ });
200
225
 
201
- const asRoleSummary = (row: RoleRow): RoleSummary => ({ ...row, id: row.id.toString() });
226
+ const asRoleSummary = (row: RoleRow): RoleSummary => ({
227
+ ...row,
228
+ id: row.id.toString(),
229
+ });
202
230
 
203
231
  /**
204
232
  * The cache subject in this variant: authorization belongs to a membership, so the key has to be
@@ -209,7 +237,8 @@ const asRoleSummary = (row: RoleRow): RoleSummary => ({ ...row, id: row.id.toStr
209
237
  * between the two one-directional — the guard needs this repository at runtime, so the repository
210
238
  * must not need the guard.
211
239
  */
212
- export const memberCacheKey = (userId: string, workspaceId: string) => `${userId}:${workspaceId}`;
240
+ export const memberCacheKey = (userId: string, workspaceId: string) =>
241
+ `${userId}:${workspaceId}`;
213
242
 
214
243
  @Injectable()
215
244
  export class RbacRepository {
@@ -234,11 +263,18 @@ export class RbacRepository {
234
263
  // hand from its own database lookup, so there's nothing to parse here.
235
264
  private async invalidateMemberById(memberId: bigint): Promise<void> {
236
265
  const [member] = await this.db
237
- .select({ userId: workspaceMembers.userId, workspaceId: workspaceMembers.workspaceId })
266
+ .select({
267
+ userId: workspaceMembers.userId,
268
+ workspaceId: workspaceMembers.workspaceId,
269
+ })
238
270
  .from(workspaceMembers)
239
271
  .where(eq(workspaceMembers.id, memberId))
240
272
  .limit(1);
241
- if (member) await this.invalidateMember(member.userId.toString(), member.workspaceId.toString());
273
+ if (member)
274
+ await this.invalidateMember(
275
+ member.userId.toString(),
276
+ member.workspaceId.toString(),
277
+ );
242
278
  }
243
279
 
244
280
  /**
@@ -256,7 +292,10 @@ export class RbacRepository {
256
292
  * `isActive: false` on a role or a permission takes it out of the answer, which is what makes
257
293
  * deactivating either one enforcement-changing without unpicking any grant.
258
294
  */
259
- async resolveAuthzContext(userId: string, workspaceId: string): Promise<AuthzContext | null> {
295
+ async resolveAuthzContext(
296
+ userId: string,
297
+ workspaceId: string,
298
+ ): Promise<AuthzContext | null> {
260
299
  // workspaceId arrives straight from the untrusted `X-Workspace-Id` header — a value that
261
300
  // isn't a valid bigint is exactly as "not a member" as one that parses but names no
262
301
  // membership, so it must not throw and 500 the request.
@@ -272,8 +311,16 @@ export class RbacRepository {
272
311
  const [member] = await this.db
273
312
  .select({ id: workspaceMembers.id })
274
313
  .from(workspaceMembers)
275
- .innerJoin(users, and(eq(users.id, workspaceMembers.userId), eq(users.isDeleted, false)))
276
- .where(and(eq(workspaceMembers.userId, userIdBig), eq(workspaceMembers.workspaceId, workspaceIdBig)))
314
+ .innerJoin(
315
+ users,
316
+ and(eq(users.id, workspaceMembers.userId), eq(users.isDeleted, false)),
317
+ )
318
+ .where(
319
+ and(
320
+ eq(workspaceMembers.userId, userIdBig),
321
+ eq(workspaceMembers.workspaceId, workspaceIdBig),
322
+ ),
323
+ )
277
324
  .limit(1);
278
325
  if (!member) return null;
279
326
 
@@ -283,14 +330,35 @@ export class RbacRepository {
283
330
  this.db
284
331
  .select({ roleSlug: roles.slug, permissionSlug: permissions.slug })
285
332
  .from(roleMember)
286
- .innerJoin(roles, and(eq(roles.id, roleMember.roleId), eq(roles.isActive, true), eq(roles.isDeleted, false)))
333
+ .innerJoin(
334
+ roles,
335
+ and(
336
+ eq(roles.id, roleMember.roleId),
337
+ eq(roles.isActive, true),
338
+ eq(roles.isDeleted, false),
339
+ ),
340
+ )
287
341
  .leftJoin(permissionRole, eq(permissionRole.roleId, roles.id))
288
- .leftJoin(permissions, and(eq(permissions.id, permissionRole.permissionId), eq(permissions.isActive, true), eq(permissions.isDeleted, false)))
342
+ .leftJoin(
343
+ permissions,
344
+ and(
345
+ eq(permissions.id, permissionRole.permissionId),
346
+ eq(permissions.isActive, true),
347
+ eq(permissions.isDeleted, false),
348
+ ),
349
+ )
289
350
  .where(eq(roleMember.memberId, member.id)),
290
351
  this.db
291
352
  .select({ slug: permissions.slug })
292
353
  .from(permissionMember)
293
- .innerJoin(permissions, and(eq(permissions.id, permissionMember.permissionId), eq(permissions.isActive, true), eq(permissions.isDeleted, false)))
354
+ .innerJoin(
355
+ permissions,
356
+ and(
357
+ eq(permissions.id, permissionMember.permissionId),
358
+ eq(permissions.isActive, true),
359
+ eq(permissions.isDeleted, false),
360
+ ),
361
+ )
294
362
  .where(eq(permissionMember.memberId, member.id)),
295
363
  ]);
296
364
 
@@ -299,7 +367,9 @@ export class RbacRepository {
299
367
  memberId: member.id.toString(),
300
368
  roles: [...new Set(roleRows.map((row) => row.roleSlug))].sort(),
301
369
  permissions: resolvePermissions(
302
- roleRows.map((row) => row.permissionSlug).filter((slug): slug is string => slug !== null),
370
+ roleRows
371
+ .map((row) => row.permissionSlug)
372
+ .filter((slug): slug is string => slug !== null),
303
373
  directRows.map((row) => row.slug),
304
374
  ),
305
375
  };
@@ -311,13 +381,22 @@ export class RbacRepository {
311
381
  * caller feeds the id straight into another database call, and the one external caller
312
382
  * (AuthService) discards the result.
313
383
  */
314
- async requireMember(workspaceId: string, userId: string): Promise<{ id: bigint }> {
384
+ async requireMember(
385
+ workspaceId: string,
386
+ userId: string,
387
+ ): Promise<{ id: bigint }> {
315
388
  const [member] = await this.db
316
389
  .select({ id: workspaceMembers.id })
317
390
  .from(workspaceMembers)
318
- .where(and(eq(workspaceMembers.userId, toId(userId)), eq(workspaceMembers.workspaceId, toId(workspaceId))))
391
+ .where(
392
+ and(
393
+ eq(workspaceMembers.userId, toId(userId)),
394
+ eq(workspaceMembers.workspaceId, toId(workspaceId)),
395
+ ),
396
+ )
319
397
  .limit(1);
320
- if (!member) throw new NotFoundException("user is not a member of this workspace");
398
+ if (!member)
399
+ throw new NotFoundException("user is not a member of this workspace");
321
400
  return member;
322
401
  }
323
402
 
@@ -325,13 +404,20 @@ export class RbacRepository {
325
404
  * Newest-first, page-paginated; `search` matches an email substring. Returns raw rows —
326
405
  * shaping the collection is the caller's job, see `toMemberSummary`.
327
406
  */
328
- async listMembers(workspaceId: string, filter: MemberListFilter = {}): Promise<Paginated<MemberRow>> {
407
+ async listMembers(
408
+ workspaceId: string,
409
+ filter: MemberListFilter = {},
410
+ ): Promise<Paginated<MemberRow>> {
329
411
  const page = normalizePage(filter.page);
330
412
  const limit = normalizeLimit(filter.limit);
331
413
  const workspaceIdBig = toId(workspaceId);
332
414
 
333
- const conditions: SQL[] = [eq(workspaceMembers.workspaceId, workspaceIdBig), eq(users.isDeleted, false)];
334
- if (filter.search) conditions.push(ilike(users.email, `%${filter.search}%`));
415
+ const conditions: SQL[] = [
416
+ eq(workspaceMembers.workspaceId, workspaceIdBig),
417
+ eq(users.isDeleted, false),
418
+ ];
419
+ if (filter.search)
420
+ conditions.push(ilike(users.email, `%${filter.search}%`));
335
421
  const where = and(...conditions)!;
336
422
 
337
423
  const rows = await this.db
@@ -368,7 +454,10 @@ export class RbacRepository {
368
454
  }
369
455
 
370
456
  return {
371
- items: rows.map((row) => ({ ...row, roles: (rolesByMember.get(row.memberId.toString()) ?? []).sort() })),
457
+ items: rows.map((row) => ({
458
+ ...row,
459
+ roles: (rolesByMember.get(row.memberId.toString()) ?? []).sort(),
460
+ })),
372
461
  meta: buildPageMeta(page, limit, total),
373
462
  };
374
463
  }
@@ -385,14 +474,24 @@ export class RbacRepository {
385
474
  const [row] = await this.db
386
475
  .select(MEMBER_SELECT)
387
476
  .from(workspaceMembers)
388
- .innerJoin(users, and(eq(users.id, workspaceMembers.userId), eq(users.isDeleted, false)))
477
+ .innerJoin(
478
+ users,
479
+ and(eq(users.id, workspaceMembers.userId), eq(users.isDeleted, false)),
480
+ )
389
481
  .where(eq(workspaceMembers.id, member.id))
390
482
  .limit(1);
391
483
  if (!row) throw new NotFoundException(`user "${userId}" not found`);
392
484
 
393
- const assignments = await this.db.select({ slug: roles.slug }).from(roleMember).innerJoin(roles, eq(roles.id, roleMember.roleId)).where(eq(roleMember.memberId, row.memberId));
485
+ const assignments = await this.db
486
+ .select({ slug: roles.slug })
487
+ .from(roleMember)
488
+ .innerJoin(roles, eq(roles.id, roleMember.roleId))
489
+ .where(eq(roleMember.memberId, row.memberId));
394
490
 
395
- return toMemberSummary({ ...row, roles: assignments.map((a) => a.slug).sort() });
491
+ return toMemberSummary({
492
+ ...row,
493
+ roles: assignments.map((a) => a.slug).sort(),
494
+ });
396
495
  }
397
496
 
398
497
  /** Profile fields only — email is the login identifier and stays out of this endpoint. */
@@ -413,7 +512,9 @@ export class RbacRepository {
413
512
  actorUserId: string | null,
414
513
  ): Promise<MemberSummary> {
415
514
  await this.requireMember(workspaceId, userId);
416
- const changed = Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
515
+ const changed = Object.fromEntries(
516
+ Object.entries(input).filter(([, value]) => value !== undefined),
517
+ );
417
518
  await this.db
418
519
  .update(users)
419
520
  .set({ ...changed, updatedBy: toIdOrNull(actorUserId) })
@@ -427,11 +528,21 @@ export class RbacRepository {
427
528
  * member's account — this disables the account across every workspace it belongs to, not just
428
529
  * this one, which is why it's gated the same way `block` is.
429
530
  */
430
- async deleteMember(workspaceId: string, userId: string, actorUserId: string | null, reason?: string): Promise<void> {
531
+ async deleteMember(
532
+ workspaceId: string,
533
+ userId: string,
534
+ actorUserId: string | null,
535
+ reason?: string,
536
+ ): Promise<void> {
431
537
  await this.requireMember(workspaceId, userId);
432
538
  await this.db
433
539
  .update(users)
434
- .set({ isDeleted: true, deletedAt: new Date(), deletedBy: toIdOrNull(actorUserId), deletedReason: reason ?? null })
540
+ .set({
541
+ isDeleted: true,
542
+ deletedAt: new Date(),
543
+ deletedBy: toIdOrNull(actorUserId),
544
+ deletedReason: reason ?? null,
545
+ })
435
546
  .where(eq(users.id, toId(userId)));
436
547
  await this.invalidateMember(userId, workspaceId);
437
548
  }
@@ -448,7 +559,12 @@ export class RbacRepository {
448
559
  .select(PERMISSION_COLUMNS)
449
560
  .from(permissions)
450
561
  .where(eq(permissions.isDeleted, false))
451
- .orderBy(asc(permissions.groupOrder), asc(permissions.group), asc(permissions.order), asc(permissions.slug));
562
+ .orderBy(
563
+ asc(permissions.groupOrder),
564
+ asc(permissions.group),
565
+ asc(permissions.order),
566
+ asc(permissions.slug),
567
+ );
452
568
  return rows.map(asPermissionSummary);
453
569
  }
454
570
 
@@ -461,7 +577,10 @@ export class RbacRepository {
461
577
  * Upserted on `slug`, which stays the stable identifier grants and revocations use, so renaming
462
578
  * `name`/`displayName` never breaks a grant.
463
579
  */
464
- async upsertPermission(input: PermissionInput, actorUserId: string | null): Promise<PermissionSummary> {
580
+ async upsertPermission(
581
+ input: PermissionInput,
582
+ actorUserId: string | null,
583
+ ): Promise<PermissionSummary> {
465
584
  const actorId = toIdOrNull(actorUserId);
466
585
  // Only the fields the caller sent are written; an absent one leaves its column alone, so a
467
586
  // partial edit cannot blank out metadata it did not mention.
@@ -492,7 +611,9 @@ export class RbacRepository {
492
611
  // returns none — so the no-op case rewrites the slug with itself.
493
612
  .onConflictDoUpdate({
494
613
  target: permissions.slug,
495
- set: Object.keys(changed).length ? { ...changed, updatedBy: actorId } : { slug: input.slug, updatedBy: actorId },
614
+ set: Object.keys(changed).length
615
+ ? { ...changed, updatedBy: actorId }
616
+ : { slug: input.slug, updatedBy: actorId },
496
617
  })
497
618
  .returning(PERMISSION_COLUMNS);
498
619
 
@@ -505,14 +626,26 @@ export class RbacRepository {
505
626
  const rows = await this.db
506
627
  .select(ROLE_COLUMNS)
507
628
  .from(roles)
508
- .where(and(eq(roles.workspaceId, toId(workspaceId)), eq(roles.isDeleted, false)))
629
+ .where(
630
+ and(
631
+ eq(roles.workspaceId, toId(workspaceId)),
632
+ eq(roles.isDeleted, false),
633
+ ),
634
+ )
509
635
  .orderBy(asc(roles.order), asc(roles.slug));
510
636
  return rows.map(asRoleSummary);
511
637
  }
512
638
 
513
639
  async createRole(
514
640
  workspaceId: string,
515
- input: { slug: string; name?: string; displayName?: string; description?: string | null; isDefault?: boolean; isActive?: boolean },
641
+ input: {
642
+ slug: string;
643
+ name?: string;
644
+ displayName?: string;
645
+ description?: string | null;
646
+ isDefault?: boolean;
647
+ isActive?: boolean;
648
+ },
516
649
  actorUserId: string | null,
517
650
  ): Promise<RoleSummary> {
518
651
  const actorId = toIdOrNull(actorUserId);
@@ -536,7 +669,13 @@ export class RbacRepository {
536
669
  async updateRole(
537
670
  workspaceId: string,
538
671
  roleId: string,
539
- input: { name?: string; displayName?: string; description?: string | null; isDefault?: boolean; isActive?: boolean },
672
+ input: {
673
+ name?: string;
674
+ displayName?: string;
675
+ description?: string | null;
676
+ isDefault?: boolean;
677
+ isActive?: boolean;
678
+ },
540
679
  actorUserId: string | null,
541
680
  ): Promise<RoleSummary> {
542
681
  const workspaceIdBig = toId(workspaceId);
@@ -546,9 +685,12 @@ export class RbacRepository {
546
685
  .from(roles)
547
686
  .where(and(eq(roles.id, roleIdBig), eq(roles.isDeleted, false)))
548
687
  .limit(1);
549
- if (!existing || existing.workspaceId !== workspaceIdBig) throw new NotFoundException("role not found in this workspace");
688
+ if (!existing || existing.workspaceId !== workspaceIdBig)
689
+ throw new NotFoundException("role not found in this workspace");
550
690
 
551
- const changed = Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
691
+ const changed = Object.fromEntries(
692
+ Object.entries(input).filter(([, value]) => value !== undefined),
693
+ );
552
694
  const [role] = await this.db
553
695
  .update(roles)
554
696
  .set({ ...changed, updatedBy: toIdOrNull(actorUserId) })
@@ -562,7 +704,12 @@ export class RbacRepository {
562
704
  * Soft-delete. `role_member`/`permission_role` rows pointing at it are left in place — the role
563
705
  * simply stops being resolved by `resolveAuthzContext`/`listRoles`.
564
706
  */
565
- async deleteRole(workspaceId: string, roleId: string, actorUserId: string | null, reason?: string): Promise<void> {
707
+ async deleteRole(
708
+ workspaceId: string,
709
+ roleId: string,
710
+ actorUserId: string | null,
711
+ reason?: string,
712
+ ): Promise<void> {
566
713
  const workspaceIdBig = toId(workspaceId);
567
714
  const roleIdBig = toId(roleId);
568
715
  const [existing] = await this.db
@@ -570,11 +717,17 @@ export class RbacRepository {
570
717
  .from(roles)
571
718
  .where(and(eq(roles.id, roleIdBig), eq(roles.isDeleted, false)))
572
719
  .limit(1);
573
- if (!existing || existing.workspaceId !== workspaceIdBig) throw new NotFoundException("role not found in this workspace");
720
+ if (!existing || existing.workspaceId !== workspaceIdBig)
721
+ throw new NotFoundException("role not found in this workspace");
574
722
 
575
723
  await this.db
576
724
  .update(roles)
577
- .set({ isDeleted: true, deletedAt: new Date(), deletedBy: toIdOrNull(actorUserId), deletedReason: reason ?? null })
725
+ .set({
726
+ isDeleted: true,
727
+ deletedAt: new Date(),
728
+ deletedBy: toIdOrNull(actorUserId),
729
+ deletedReason: reason ?? null,
730
+ })
578
731
  .where(eq(roles.id, roleIdBig));
579
732
  await this.cache.invalidatePolicy();
580
733
  }
@@ -585,7 +738,12 @@ export class RbacRepository {
585
738
  * be granted, and it opens nothing until a route names it — and no route shipped by this
586
739
  * library can name it, because `@CheckAbility` only accepts slugs from `rbac.defaults.ts`.
587
740
  */
588
- async attachPermissionToRole(workspaceId: string, roleId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
741
+ async attachPermissionToRole(
742
+ workspaceId: string,
743
+ roleId: string,
744
+ permissionSlug: string,
745
+ actorUserId: string | null,
746
+ ): Promise<void> {
589
747
  const workspaceIdBig = toId(workspaceId);
590
748
  const roleIdBig = toId(roleId);
591
749
  const [role] = await this.db
@@ -593,57 +751,94 @@ export class RbacRepository {
593
751
  .from(roles)
594
752
  .where(and(eq(roles.id, roleIdBig), eq(roles.isDeleted, false)))
595
753
  .limit(1);
596
- if (!role || role.workspaceId !== workspaceIdBig) throw new NotFoundException("role not found in this workspace");
754
+ if (!role || role.workspaceId !== workspaceIdBig)
755
+ throw new NotFoundException("role not found in this workspace");
597
756
 
598
757
  const permission = await this.ensurePermission(permissionSlug, actorUserId);
599
758
  await this.db
600
759
  .insert(permissionRole)
601
760
  .values({ permissionId: permission.id, roleId: roleIdBig })
602
- .onConflictDoNothing({ target: [permissionRole.permissionId, permissionRole.roleId] });
761
+ .onConflictDoNothing({
762
+ target: [permissionRole.permissionId, permissionRole.roleId],
763
+ });
603
764
  await this.cache.invalidatePolicy(); // every membership holding this role is affected
604
765
  }
605
766
 
606
- async assignRoleToMember(workspaceId: string, userId: string, roleSlug: string): Promise<void> {
767
+ async assignRoleToMember(
768
+ workspaceId: string,
769
+ userId: string,
770
+ roleSlug: string,
771
+ ): Promise<void> {
607
772
  const role = await this.requireRole(workspaceId, roleSlug);
608
773
  const member = await this.requireMember(workspaceId, userId);
609
774
  await this.db
610
775
  .insert(roleMember)
611
776
  .values({ memberId: member.id, roleId: role.id })
612
- .onConflictDoNothing({ target: [roleMember.memberId, roleMember.roleId] });
777
+ .onConflictDoNothing({
778
+ target: [roleMember.memberId, roleMember.roleId],
779
+ });
613
780
  await this.invalidateMember(userId, workspaceId);
614
781
  }
615
782
 
616
- async revokeRoleFromMember(workspaceId: string, userId: string, roleSlug: string): Promise<void> {
783
+ async revokeRoleFromMember(
784
+ workspaceId: string,
785
+ userId: string,
786
+ roleSlug: string,
787
+ ): Promise<void> {
617
788
  const member = await this.requireMember(workspaceId, userId);
618
789
  const [role] = await this.db
619
790
  .select({ id: roles.id })
620
791
  .from(roles)
621
- .where(and(eq(roles.workspaceId, toId(workspaceId)), eq(roles.slug, roleSlug)))
792
+ .where(
793
+ and(eq(roles.workspaceId, toId(workspaceId)), eq(roles.slug, roleSlug)),
794
+ )
622
795
  .limit(1);
623
796
  if (!role) return;
624
- await this.db.delete(roleMember).where(and(eq(roleMember.memberId, member.id), eq(roleMember.roleId, role.id)));
797
+ await this.db
798
+ .delete(roleMember)
799
+ .where(
800
+ and(eq(roleMember.memberId, member.id), eq(roleMember.roleId, role.id)),
801
+ );
625
802
  await this.invalidateMember(userId, workspaceId);
626
803
  }
627
804
 
628
805
  /** Replaces the whole set — the "set a member's roles" operation, as opposed to assign/revoke one at a time. */
629
- async setMemberRoles(workspaceId: string, memberId: string, roleSlugs: string[]): Promise<{ memberId: string; roles: string[] }> {
806
+ async setMemberRoles(
807
+ workspaceId: string,
808
+ memberId: string,
809
+ roleSlugs: string[],
810
+ ): Promise<{ memberId: string; roles: string[] }> {
630
811
  const workspaceIdBig = toId(workspaceId);
631
812
  const memberIdBig = toId(memberId);
632
813
  const [member] = await this.db
633
- .select({ id: workspaceMembers.id, workspaceId: workspaceMembers.workspaceId })
814
+ .select({
815
+ id: workspaceMembers.id,
816
+ workspaceId: workspaceMembers.workspaceId,
817
+ })
634
818
  .from(workspaceMembers)
635
819
  .where(eq(workspaceMembers.id, memberIdBig))
636
820
  .limit(1);
637
- if (!member || member.workspaceId !== workspaceIdBig) throw new NotFoundException("member not found in this workspace");
821
+ if (!member || member.workspaceId !== workspaceIdBig)
822
+ throw new NotFoundException("member not found in this workspace");
638
823
 
639
824
  const found = roleSlugs.length
640
825
  ? await this.db
641
826
  .select({ id: roles.id, slug: roles.slug })
642
827
  .from(roles)
643
- .where(and(eq(roles.workspaceId, workspaceIdBig), inArray(roles.slug, roleSlugs)))
828
+ .where(
829
+ and(
830
+ eq(roles.workspaceId, workspaceIdBig),
831
+ inArray(roles.slug, roleSlugs),
832
+ ),
833
+ )
644
834
  : [];
645
- const unknown = roleSlugs.filter((slug) => !found.some((role) => role.slug === slug));
646
- if (unknown.length) throw new NotFoundException(`role(s) not defined in this workspace: ${unknown.join(", ")}`);
835
+ const unknown = roleSlugs.filter(
836
+ (slug) => !found.some((role) => role.slug === slug),
837
+ );
838
+ if (unknown.length)
839
+ throw new NotFoundException(
840
+ `role(s) not defined in this workspace: ${unknown.join(", ")}`,
841
+ );
647
842
 
648
843
  // Replace, in one transaction: a member must never be briefly role-less to a concurrent request.
649
844
  await this.db.transaction(async (tx) => {
@@ -651,8 +846,12 @@ export class RbacRepository {
651
846
  if (found.length) {
652
847
  await tx
653
848
  .insert(roleMember)
654
- .values(found.map((role) => ({ memberId: memberIdBig, roleId: role.id })))
655
- .onConflictDoNothing({ target: [roleMember.memberId, roleMember.roleId] });
849
+ .values(
850
+ found.map((role) => ({ memberId: memberIdBig, roleId: role.id })),
851
+ )
852
+ .onConflictDoNothing({
853
+ target: [roleMember.memberId, roleMember.roleId],
854
+ });
656
855
  }
657
856
  });
658
857
  await this.invalidateMemberById(memberIdBig);
@@ -668,37 +867,76 @@ export class RbacRepository {
668
867
  const rows = await this.db
669
868
  .select({ id: roles.id })
670
869
  .from(roles)
671
- .where(and(eq(roles.workspaceId, toId(workspaceId)), eq(roles.isDefault, true), eq(roles.isActive, true), eq(roles.isDeleted, false)));
870
+ .where(
871
+ and(
872
+ eq(roles.workspaceId, toId(workspaceId)),
873
+ eq(roles.isDefault, true),
874
+ eq(roles.isActive, true),
875
+ eq(roles.isDeleted, false),
876
+ ),
877
+ );
672
878
  return rows.map((role) => role.id);
673
879
  }
674
880
 
675
- async grantPermissionToMember(workspaceId: string, userId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
881
+ async grantPermissionToMember(
882
+ workspaceId: string,
883
+ userId: string,
884
+ permissionSlug: string,
885
+ actorUserId: string | null,
886
+ ): Promise<void> {
676
887
  const member = await this.requireMember(workspaceId, userId);
677
888
  const permission = await this.ensurePermission(permissionSlug, actorUserId);
678
889
  await this.db
679
890
  .insert(permissionMember)
680
891
  .values({ memberId: member.id, permissionId: permission.id })
681
- .onConflictDoNothing({ target: [permissionMember.memberId, permissionMember.permissionId] });
892
+ .onConflictDoNothing({
893
+ target: [permissionMember.memberId, permissionMember.permissionId],
894
+ });
682
895
  await this.invalidateMember(userId, workspaceId);
683
896
  }
684
897
 
685
- async revokePermissionFromMember(workspaceId: string, userId: string, permissionSlug: string): Promise<void> {
898
+ async revokePermissionFromMember(
899
+ workspaceId: string,
900
+ userId: string,
901
+ permissionSlug: string,
902
+ ): Promise<void> {
686
903
  const member = await this.requireMember(workspaceId, userId);
687
- const [permission] = await this.db.select({ id: permissions.id }).from(permissions).where(eq(permissions.slug, permissionSlug)).limit(1);
904
+ const [permission] = await this.db
905
+ .select({ id: permissions.id })
906
+ .from(permissions)
907
+ .where(eq(permissions.slug, permissionSlug))
908
+ .limit(1);
688
909
  if (!permission) return;
689
- await this.db.delete(permissionMember).where(and(eq(permissionMember.memberId, member.id), eq(permissionMember.permissionId, permission.id)));
910
+ await this.db
911
+ .delete(permissionMember)
912
+ .where(
913
+ and(
914
+ eq(permissionMember.memberId, member.id),
915
+ eq(permissionMember.permissionId, permission.id),
916
+ ),
917
+ );
690
918
  await this.invalidateMember(userId, workspaceId);
691
919
  }
692
920
 
693
921
  // Returns bigint directly: its one caller (assignRoleToMember) feeds the id straight into
694
922
  // another database call.
695
- private async requireRole(workspaceId: string, slug: string): Promise<{ id: bigint }> {
923
+ private async requireRole(
924
+ workspaceId: string,
925
+ slug: string,
926
+ ): Promise<{ id: bigint }> {
696
927
  const [role] = await this.db
697
928
  .select({ id: roles.id })
698
929
  .from(roles)
699
- .where(and(eq(roles.workspaceId, toId(workspaceId)), eq(roles.slug, slug), eq(roles.isDeleted, false)))
930
+ .where(
931
+ and(
932
+ eq(roles.workspaceId, toId(workspaceId)),
933
+ eq(roles.slug, slug),
934
+ eq(roles.isDeleted, false),
935
+ ),
936
+ )
700
937
  .limit(1);
701
- if (!role) throw new NotFoundException(`role "${slug}" not found in this workspace`);
938
+ if (!role)
939
+ throw new NotFoundException(`role "${slug}" not found in this workspace`);
702
940
  return role;
703
941
  }
704
942
 
@@ -706,13 +944,27 @@ export class RbacRepository {
706
944
  * Creates the row for a slug granted before anyone defined it; leaves an existing definition
707
945
  * alone. Returns bigint directly: both callers feed the id straight into another database call.
708
946
  */
709
- private async ensurePermission(slug: string, actorUserId: string | null): Promise<{ id: bigint }> {
710
- const [existing] = await this.db.select({ id: permissions.id }).from(permissions).where(eq(permissions.slug, slug)).limit(1);
947
+ private async ensurePermission(
948
+ slug: string,
949
+ actorUserId: string | null,
950
+ ): Promise<{ id: bigint }> {
951
+ const [existing] = await this.db
952
+ .select({ id: permissions.id })
953
+ .from(permissions)
954
+ .where(eq(permissions.slug, slug))
955
+ .limit(1);
711
956
  if (existing) return existing;
712
957
  const actorId = toIdOrNull(actorUserId);
713
958
  const [created] = await this.db
714
959
  .insert(permissions)
715
- .values({ slug, name: slug, displayName: slug, group: "Custom", createdBy: actorId, updatedBy: actorId })
960
+ .values({
961
+ slug,
962
+ name: slug,
963
+ displayName: slug,
964
+ group: "Custom",
965
+ createdBy: actorId,
966
+ updatedBy: actorId,
967
+ })
716
968
  .returning({ id: permissions.id });
717
969
  return created;
718
970
  }