@simple-auth-kit/cli 1.4.3 → 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 +4 -4
  116. package/simple-auth-kit.ts +29 -2
  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
- import { and, asc, count, desc, eq, ilike, inArray, type SQL } from "drizzle-orm";
1
+ import {
2
+ and,
3
+ asc,
4
+ count,
5
+ desc,
6
+ eq,
7
+ ilike,
8
+ inArray,
9
+ type SQL,
10
+ } from "drizzle-orm";
2
11
  import { resolvePermissions } from "@/lib/auth/core/rbac.js";
3
- import type { AuthzContext } from "./authz.middleware.js";
4
- import type { Database } from "./db.js";
5
- import { buildPageMeta, normalizeLimit, normalizePage, type Paginated } from "./pagination.js";
6
- import { PermissionCache } from "./permission-cache.js";
7
- import { permissionMember, permissionRole, permissions, roleMember, roles, users, workspaceMembers } from "./schema.js";
8
- import { HttpError } from "./http-error.js";
9
- import { toId, toIdOrNull } from "./id.helper.js";
12
+ import type { AuthzContext } from "../middleware/authz.middleware.js";
13
+ import type { Database } from "../db.js";
14
+ import {
15
+ buildPageMeta,
16
+ normalizeLimit,
17
+ normalizePage,
18
+ type Paginated,
19
+ } from "../pagination.js";
20
+ import { PermissionCache } from "../permission-cache.js";
21
+ import {
22
+ permissionMember,
23
+ permissionRole,
24
+ permissions,
25
+ roleMember,
26
+ roles,
27
+ users,
28
+ workspaceMembers,
29
+ } from "../schema.js";
30
+ import { HttpError } from "../http-error.js";
31
+ import { toId, toIdOrNull } from "../id.helper.js";
10
32
 
11
33
  /**
12
34
  * Every column the `users` table has (plus the membership's own `memberId`/`createdAt`), except
@@ -187,9 +209,15 @@ interface RoleRow {
187
209
  isActive: boolean;
188
210
  }
189
211
 
190
- const asPermissionSummary = (row: PermissionRow): PermissionSummary => ({ ...row, id: row.id.toString() });
212
+ const asPermissionSummary = (row: PermissionRow): PermissionSummary => ({
213
+ ...row,
214
+ id: row.id.toString(),
215
+ });
191
216
 
192
- const asRoleSummary = (row: RoleRow): RoleSummary => ({ ...row, id: row.id.toString() });
217
+ const asRoleSummary = (row: RoleRow): RoleSummary => ({
218
+ ...row,
219
+ id: row.id.toString(),
220
+ });
193
221
 
194
222
  /**
195
223
  * The cache subject in this variant: authorization belongs to a membership, so the key has to be
@@ -200,7 +228,8 @@ const asRoleSummary = (row: RoleRow): RoleSummary => ({ ...row, id: row.id.toStr
200
228
  * between the two one-directional — the guard needs this repository at runtime, so the repository
201
229
  * must not need the guard.
202
230
  */
203
- export const memberCacheKey = (userId: string, workspaceId: string) => `${userId}:${workspaceId}`;
231
+ export const memberCacheKey = (userId: string, workspaceId: string) =>
232
+ `${userId}:${workspaceId}`;
204
233
 
205
234
  export class RbacRepository {
206
235
  constructor(
@@ -224,11 +253,18 @@ export class RbacRepository {
224
253
  // hand from its own database lookup, so there's nothing to parse here.
225
254
  private async invalidateMemberById(memberId: bigint): Promise<void> {
226
255
  const [member] = await this.db
227
- .select({ userId: workspaceMembers.userId, workspaceId: workspaceMembers.workspaceId })
256
+ .select({
257
+ userId: workspaceMembers.userId,
258
+ workspaceId: workspaceMembers.workspaceId,
259
+ })
228
260
  .from(workspaceMembers)
229
261
  .where(eq(workspaceMembers.id, memberId))
230
262
  .limit(1);
231
- if (member) await this.invalidateMember(member.userId.toString(), member.workspaceId.toString());
263
+ if (member)
264
+ await this.invalidateMember(
265
+ member.userId.toString(),
266
+ member.workspaceId.toString(),
267
+ );
232
268
  }
233
269
 
234
270
  /**
@@ -246,7 +282,10 @@ export class RbacRepository {
246
282
  * `isActive: false` on a role or a permission takes it out of the answer, which is what makes
247
283
  * deactivating either one enforcement-changing without unpicking any grant.
248
284
  */
249
- async resolveAuthzContext(userId: string, workspaceId: string): Promise<AuthzContext | null> {
285
+ async resolveAuthzContext(
286
+ userId: string,
287
+ workspaceId: string,
288
+ ): Promise<AuthzContext | null> {
250
289
  // workspaceId arrives straight from the untrusted `X-Workspace-Id` header — a value that
251
290
  // isn't a valid bigint is exactly as "not a member" as one that parses but names no
252
291
  // membership, so it must not throw and 500 the request.
@@ -262,8 +301,16 @@ export class RbacRepository {
262
301
  const [member] = await this.db
263
302
  .select({ id: workspaceMembers.id })
264
303
  .from(workspaceMembers)
265
- .innerJoin(users, and(eq(users.id, workspaceMembers.userId), eq(users.isDeleted, false)))
266
- .where(and(eq(workspaceMembers.userId, userIdBig), eq(workspaceMembers.workspaceId, workspaceIdBig)))
304
+ .innerJoin(
305
+ users,
306
+ and(eq(users.id, workspaceMembers.userId), eq(users.isDeleted, false)),
307
+ )
308
+ .where(
309
+ and(
310
+ eq(workspaceMembers.userId, userIdBig),
311
+ eq(workspaceMembers.workspaceId, workspaceIdBig),
312
+ ),
313
+ )
267
314
  .limit(1);
268
315
  if (!member) return null;
269
316
 
@@ -273,14 +320,35 @@ export class RbacRepository {
273
320
  this.db
274
321
  .select({ roleSlug: roles.slug, permissionSlug: permissions.slug })
275
322
  .from(roleMember)
276
- .innerJoin(roles, and(eq(roles.id, roleMember.roleId), eq(roles.isActive, true), eq(roles.isDeleted, false)))
323
+ .innerJoin(
324
+ roles,
325
+ and(
326
+ eq(roles.id, roleMember.roleId),
327
+ eq(roles.isActive, true),
328
+ eq(roles.isDeleted, false),
329
+ ),
330
+ )
277
331
  .leftJoin(permissionRole, eq(permissionRole.roleId, roles.id))
278
- .leftJoin(permissions, and(eq(permissions.id, permissionRole.permissionId), eq(permissions.isActive, true), eq(permissions.isDeleted, false)))
332
+ .leftJoin(
333
+ permissions,
334
+ and(
335
+ eq(permissions.id, permissionRole.permissionId),
336
+ eq(permissions.isActive, true),
337
+ eq(permissions.isDeleted, false),
338
+ ),
339
+ )
279
340
  .where(eq(roleMember.memberId, member.id)),
280
341
  this.db
281
342
  .select({ slug: permissions.slug })
282
343
  .from(permissionMember)
283
- .innerJoin(permissions, and(eq(permissions.id, permissionMember.permissionId), eq(permissions.isActive, true), eq(permissions.isDeleted, false)))
344
+ .innerJoin(
345
+ permissions,
346
+ and(
347
+ eq(permissions.id, permissionMember.permissionId),
348
+ eq(permissions.isActive, true),
349
+ eq(permissions.isDeleted, false),
350
+ ),
351
+ )
284
352
  .where(eq(permissionMember.memberId, member.id)),
285
353
  ]);
286
354
 
@@ -289,7 +357,9 @@ export class RbacRepository {
289
357
  memberId: member.id.toString(),
290
358
  roles: [...new Set(roleRows.map((row) => row.roleSlug))].sort(),
291
359
  permissions: resolvePermissions(
292
- roleRows.map((row) => row.permissionSlug).filter((slug): slug is string => slug !== null),
360
+ roleRows
361
+ .map((row) => row.permissionSlug)
362
+ .filter((slug): slug is string => slug !== null),
293
363
  directRows.map((row) => row.slug),
294
364
  ),
295
365
  };
@@ -301,13 +371,22 @@ export class RbacRepository {
301
371
  * caller feeds the id straight into another database call, and the one external caller
302
372
  * (AuthService) discards the result.
303
373
  */
304
- async requireMember(workspaceId: string, userId: string): Promise<{ id: bigint }> {
374
+ async requireMember(
375
+ workspaceId: string,
376
+ userId: string,
377
+ ): Promise<{ id: bigint }> {
305
378
  const [member] = await this.db
306
379
  .select({ id: workspaceMembers.id })
307
380
  .from(workspaceMembers)
308
- .where(and(eq(workspaceMembers.userId, toId(userId)), eq(workspaceMembers.workspaceId, toId(workspaceId))))
381
+ .where(
382
+ and(
383
+ eq(workspaceMembers.userId, toId(userId)),
384
+ eq(workspaceMembers.workspaceId, toId(workspaceId)),
385
+ ),
386
+ )
309
387
  .limit(1);
310
- if (!member) throw new HttpError(404, "user is not a member of this workspace");
388
+ if (!member)
389
+ throw new HttpError(404, "user is not a member of this workspace");
311
390
  return member;
312
391
  }
313
392
 
@@ -315,12 +394,19 @@ export class RbacRepository {
315
394
  * Newest-first, page-paginated; `search` matches an email substring. Returns raw rows —
316
395
  * shaping the collection is the caller's job, see `toMemberSummary`.
317
396
  */
318
- async listMembers(workspaceId: string, filter: MemberListFilter = {}): Promise<Paginated<MemberRow>> {
397
+ async listMembers(
398
+ workspaceId: string,
399
+ filter: MemberListFilter = {},
400
+ ): Promise<Paginated<MemberRow>> {
319
401
  const page = normalizePage(filter.page);
320
402
  const limit = normalizeLimit(filter.limit);
321
403
  const workspaceIdBig = toId(workspaceId);
322
- const conditions: SQL[] = [eq(workspaceMembers.workspaceId, workspaceIdBig), eq(users.isDeleted, false)];
323
- if (filter.search) conditions.push(ilike(users.email, `%${filter.search}%`));
404
+ const conditions: SQL[] = [
405
+ eq(workspaceMembers.workspaceId, workspaceIdBig),
406
+ eq(users.isDeleted, false),
407
+ ];
408
+ if (filter.search)
409
+ conditions.push(ilike(users.email, `%${filter.search}%`));
324
410
  const where = and(...conditions)!;
325
411
 
326
412
  const rows = await this.db
@@ -357,7 +443,10 @@ export class RbacRepository {
357
443
  }
358
444
 
359
445
  return {
360
- items: rows.map((row) => ({ ...row, roles: (rolesByMember.get(row.memberId.toString()) ?? []).sort() })),
446
+ items: rows.map((row) => ({
447
+ ...row,
448
+ roles: (rolesByMember.get(row.memberId.toString()) ?? []).sort(),
449
+ })),
361
450
  meta: buildPageMeta(page, limit, total),
362
451
  };
363
452
  }
@@ -374,7 +463,10 @@ export class RbacRepository {
374
463
  const [row] = await this.db
375
464
  .select(MEMBER_SELECT)
376
465
  .from(workspaceMembers)
377
- .innerJoin(users, and(eq(users.id, workspaceMembers.userId), eq(users.isDeleted, false)))
466
+ .innerJoin(
467
+ users,
468
+ and(eq(users.id, workspaceMembers.userId), eq(users.isDeleted, false)),
469
+ )
378
470
  .where(eq(workspaceMembers.id, member.id))
379
471
  .limit(1);
380
472
  if (!row) throw new HttpError(404, `user "${userId}" not found`);
@@ -385,14 +477,24 @@ export class RbacRepository {
385
477
  .innerJoin(roles, eq(roles.id, roleMember.roleId))
386
478
  .where(eq(roleMember.memberId, member.id));
387
479
 
388
- return toMemberSummary({ ...row, roles: roleRows.map((r) => r.slug).sort() });
480
+ return toMemberSummary({
481
+ ...row,
482
+ roles: roleRows.map((r) => r.slug).sort(),
483
+ });
389
484
  }
390
485
 
391
486
  /** Profile fields only — email is the login identifier and stays out of this endpoint. */
392
487
  async updateMember(
393
488
  workspaceId: string,
394
489
  userId: string,
395
- input: { firstName?: string | null; lastName?: string | null; displayName?: string | null; phone?: string | null; username?: string | null; photo?: string | null },
490
+ input: {
491
+ firstName?: string | null;
492
+ lastName?: string | null;
493
+ displayName?: string | null;
494
+ phone?: string | null;
495
+ username?: string | null;
496
+ photo?: string | null;
497
+ },
396
498
  actorUserId: string | null,
397
499
  ): Promise<MemberSummary> {
398
500
  await this.requireMember(workspaceId, userId);
@@ -409,11 +511,21 @@ export class RbacRepository {
409
511
  * member's account — this disables the account across every workspace it belongs to, not just
410
512
  * this one, which is why it's gated the same way `block` is.
411
513
  */
412
- async deleteMember(workspaceId: string, userId: string, actorUserId: string | null, reason?: string): Promise<void> {
514
+ async deleteMember(
515
+ workspaceId: string,
516
+ userId: string,
517
+ actorUserId: string | null,
518
+ reason?: string,
519
+ ): Promise<void> {
413
520
  await this.requireMember(workspaceId, userId);
414
521
  await this.db
415
522
  .update(users)
416
- .set({ isDeleted: true, deletedAt: new Date(), deletedBy: toIdOrNull(actorUserId), deletedReason: reason ?? null })
523
+ .set({
524
+ isDeleted: true,
525
+ deletedAt: new Date(),
526
+ deletedBy: toIdOrNull(actorUserId),
527
+ deletedReason: reason ?? null,
528
+ })
417
529
  .where(eq(users.id, toId(userId)));
418
530
  await this.invalidateMember(userId, workspaceId);
419
531
  }
@@ -421,7 +533,12 @@ export class RbacRepository {
421
533
  async updateRole(
422
534
  workspaceId: string,
423
535
  roleId: string,
424
- input: { name?: string; displayName?: string; description?: string | null; isActive?: boolean },
536
+ input: {
537
+ name?: string;
538
+ displayName?: string;
539
+ description?: string | null;
540
+ isActive?: boolean;
541
+ },
425
542
  actorUserId: string | null,
426
543
  ): Promise<RoleSummary> {
427
544
  const workspaceIdBig = toId(workspaceId);
@@ -431,7 +548,8 @@ export class RbacRepository {
431
548
  .from(roles)
432
549
  .where(and(eq(roles.id, roleIdBig), eq(roles.isDeleted, false)))
433
550
  .limit(1);
434
- if (!existing || existing.workspaceId !== workspaceIdBig) throw new HttpError(404, "role not found in this workspace");
551
+ if (!existing || existing.workspaceId !== workspaceIdBig)
552
+ throw new HttpError(404, "role not found in this workspace");
435
553
 
436
554
  const [role] = await this.db
437
555
  .update(roles)
@@ -446,7 +564,12 @@ export class RbacRepository {
446
564
  * Soft-delete. `role_member`/`permission_role` rows pointing at it are left in place — the role
447
565
  * simply stops being resolved by `resolveAuthzContext`/`listRoles`.
448
566
  */
449
- async deleteRole(workspaceId: string, roleId: string, actorUserId: string | null, reason?: string): Promise<void> {
567
+ async deleteRole(
568
+ workspaceId: string,
569
+ roleId: string,
570
+ actorUserId: string | null,
571
+ reason?: string,
572
+ ): Promise<void> {
450
573
  const workspaceIdBig = toId(workspaceId);
451
574
  const roleIdBig = toId(roleId);
452
575
  const [existing] = await this.db
@@ -454,11 +577,17 @@ export class RbacRepository {
454
577
  .from(roles)
455
578
  .where(and(eq(roles.id, roleIdBig), eq(roles.isDeleted, false)))
456
579
  .limit(1);
457
- if (!existing || existing.workspaceId !== workspaceIdBig) throw new HttpError(404, "role not found in this workspace");
580
+ if (!existing || existing.workspaceId !== workspaceIdBig)
581
+ throw new HttpError(404, "role not found in this workspace");
458
582
 
459
583
  await this.db
460
584
  .update(roles)
461
- .set({ isDeleted: true, deletedAt: new Date(), deletedBy: toIdOrNull(actorUserId), deletedReason: reason ?? null })
585
+ .set({
586
+ isDeleted: true,
587
+ deletedAt: new Date(),
588
+ deletedBy: toIdOrNull(actorUserId),
589
+ deletedReason: reason ?? null,
590
+ })
462
591
  .where(eq(roles.id, roleIdBig));
463
592
  await this.cache.invalidatePolicy();
464
593
  }
@@ -475,7 +604,12 @@ export class RbacRepository {
475
604
  .select(PERMISSION_COLUMNS)
476
605
  .from(permissions)
477
606
  .where(eq(permissions.isDeleted, false))
478
- .orderBy(asc(permissions.groupOrder), asc(permissions.group), asc(permissions.order), asc(permissions.slug));
607
+ .orderBy(
608
+ asc(permissions.groupOrder),
609
+ asc(permissions.group),
610
+ asc(permissions.order),
611
+ asc(permissions.slug),
612
+ );
479
613
  return rows.map(asPermissionSummary);
480
614
  }
481
615
 
@@ -488,7 +622,10 @@ export class RbacRepository {
488
622
  * Upserted on `slug`, which stays the stable identifier grants and revocations use, so renaming
489
623
  * `name`/`displayName` never breaks a grant.
490
624
  */
491
- async upsertPermission(input: PermissionInput, actorUserId: string | null): Promise<PermissionSummary> {
625
+ async upsertPermission(
626
+ input: PermissionInput,
627
+ actorUserId: string | null,
628
+ ): Promise<PermissionSummary> {
492
629
  const actorId = toIdOrNull(actorUserId);
493
630
  // Only the fields the caller sent are written; an absent one leaves its column alone, so a
494
631
  // partial edit cannot blank out metadata it did not mention.
@@ -519,7 +656,9 @@ export class RbacRepository {
519
656
  // returns none — so the no-op case rewrites the slug with itself.
520
657
  .onConflictDoUpdate({
521
658
  target: permissions.slug,
522
- set: Object.keys(changed).length ? { ...changed, updatedBy: actorId } : { slug: input.slug, updatedBy: actorId },
659
+ set: Object.keys(changed).length
660
+ ? { ...changed, updatedBy: actorId }
661
+ : { slug: input.slug, updatedBy: actorId },
523
662
  })
524
663
  .returning(PERMISSION_COLUMNS);
525
664
 
@@ -532,14 +671,24 @@ export class RbacRepository {
532
671
  const rows = await this.db
533
672
  .select(ROLE_COLUMNS)
534
673
  .from(roles)
535
- .where(and(eq(roles.workspaceId, toId(workspaceId)), eq(roles.isDeleted, false)))
674
+ .where(
675
+ and(
676
+ eq(roles.workspaceId, toId(workspaceId)),
677
+ eq(roles.isDeleted, false),
678
+ ),
679
+ )
536
680
  .orderBy(asc(roles.order), asc(roles.slug));
537
681
  return rows.map(asRoleSummary);
538
682
  }
539
683
 
540
684
  async createRole(
541
685
  workspaceId: string,
542
- input: { slug: string; name?: string; displayName?: string; description?: string | null },
686
+ input: {
687
+ slug: string;
688
+ name?: string;
689
+ displayName?: string;
690
+ description?: string | null;
691
+ },
543
692
  actorUserId: string | null,
544
693
  ): Promise<RoleSummary> {
545
694
  const actorId = toIdOrNull(actorUserId);
@@ -564,7 +713,12 @@ export class RbacRepository {
564
713
  * be granted, and it opens nothing until a route names it — and no route shipped by this
565
714
  * library can name it, because `@CheckAbility` only accepts slugs from `rbac.defaults.ts`.
566
715
  */
567
- async attachPermissionToRole(workspaceId: string, roleId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
716
+ async attachPermissionToRole(
717
+ workspaceId: string,
718
+ roleId: string,
719
+ permissionSlug: string,
720
+ actorUserId: string | null,
721
+ ): Promise<void> {
568
722
  const workspaceIdBig = toId(workspaceId);
569
723
  const roleIdBig = toId(roleId);
570
724
  const [role] = await this.db
@@ -572,57 +726,95 @@ export class RbacRepository {
572
726
  .from(roles)
573
727
  .where(and(eq(roles.id, roleIdBig), eq(roles.isDeleted, false)))
574
728
  .limit(1);
575
- if (!role || role.workspaceId !== workspaceIdBig) throw new HttpError(404, "role not found in this workspace");
729
+ if (!role || role.workspaceId !== workspaceIdBig)
730
+ throw new HttpError(404, "role not found in this workspace");
576
731
 
577
732
  const permission = await this.ensurePermission(permissionSlug, actorUserId);
578
733
  await this.db
579
734
  .insert(permissionRole)
580
735
  .values({ permissionId: permission.id, roleId: roleIdBig })
581
- .onConflictDoNothing({ target: [permissionRole.permissionId, permissionRole.roleId] });
736
+ .onConflictDoNothing({
737
+ target: [permissionRole.permissionId, permissionRole.roleId],
738
+ });
582
739
  await this.cache.invalidatePolicy(); // every membership holding this role is affected
583
740
  }
584
741
 
585
- async assignRoleToMember(workspaceId: string, userId: string, roleSlug: string): Promise<void> {
742
+ async assignRoleToMember(
743
+ workspaceId: string,
744
+ userId: string,
745
+ roleSlug: string,
746
+ ): Promise<void> {
586
747
  const role = await this.requireRole(workspaceId, roleSlug);
587
748
  const member = await this.requireMember(workspaceId, userId);
588
749
  await this.db
589
750
  .insert(roleMember)
590
751
  .values({ memberId: member.id, roleId: role.id })
591
- .onConflictDoNothing({ target: [roleMember.memberId, roleMember.roleId] });
752
+ .onConflictDoNothing({
753
+ target: [roleMember.memberId, roleMember.roleId],
754
+ });
592
755
  await this.invalidateMember(userId, workspaceId);
593
756
  }
594
757
 
595
- async revokeRoleFromMember(workspaceId: string, userId: string, roleSlug: string): Promise<void> {
758
+ async revokeRoleFromMember(
759
+ workspaceId: string,
760
+ userId: string,
761
+ roleSlug: string,
762
+ ): Promise<void> {
596
763
  const member = await this.requireMember(workspaceId, userId);
597
764
  const [role] = await this.db
598
765
  .select({ id: roles.id })
599
766
  .from(roles)
600
- .where(and(eq(roles.workspaceId, toId(workspaceId)), eq(roles.slug, roleSlug)))
767
+ .where(
768
+ and(eq(roles.workspaceId, toId(workspaceId)), eq(roles.slug, roleSlug)),
769
+ )
601
770
  .limit(1);
602
771
  if (!role) return;
603
- await this.db.delete(roleMember).where(and(eq(roleMember.memberId, member.id), eq(roleMember.roleId, role.id)));
772
+ await this.db
773
+ .delete(roleMember)
774
+ .where(
775
+ and(eq(roleMember.memberId, member.id), eq(roleMember.roleId, role.id)),
776
+ );
604
777
  await this.invalidateMember(userId, workspaceId);
605
778
  }
606
779
 
607
780
  /** Replaces the whole set — the "set a member's roles" operation, as opposed to assign/revoke one at a time. */
608
- async setMemberRoles(workspaceId: string, memberId: string, roleSlugs: string[]): Promise<{ memberId: string; roles: string[] }> {
781
+ async setMemberRoles(
782
+ workspaceId: string,
783
+ memberId: string,
784
+ roleSlugs: string[],
785
+ ): Promise<{ memberId: string; roles: string[] }> {
609
786
  const workspaceIdBig = toId(workspaceId);
610
787
  const memberIdBig = toId(memberId);
611
788
  const [member] = await this.db
612
- .select({ id: workspaceMembers.id, workspaceId: workspaceMembers.workspaceId })
789
+ .select({
790
+ id: workspaceMembers.id,
791
+ workspaceId: workspaceMembers.workspaceId,
792
+ })
613
793
  .from(workspaceMembers)
614
794
  .where(eq(workspaceMembers.id, memberIdBig))
615
795
  .limit(1);
616
- if (!member || member.workspaceId !== workspaceIdBig) throw new HttpError(404, "member not found in this workspace");
796
+ if (!member || member.workspaceId !== workspaceIdBig)
797
+ throw new HttpError(404, "member not found in this workspace");
617
798
 
618
799
  const found = roleSlugs.length
619
800
  ? await this.db
620
801
  .select({ id: roles.id, slug: roles.slug })
621
802
  .from(roles)
622
- .where(and(eq(roles.workspaceId, workspaceIdBig), inArray(roles.slug, roleSlugs)))
803
+ .where(
804
+ and(
805
+ eq(roles.workspaceId, workspaceIdBig),
806
+ inArray(roles.slug, roleSlugs),
807
+ ),
808
+ )
623
809
  : [];
624
- const unknown = roleSlugs.filter((slug) => !found.some((role) => role.slug === slug));
625
- if (unknown.length) throw new HttpError(404, `role(s) not defined in this workspace: ${unknown.join(", ")}`);
810
+ const unknown = roleSlugs.filter(
811
+ (slug) => !found.some((role) => role.slug === slug),
812
+ );
813
+ if (unknown.length)
814
+ throw new HttpError(
815
+ 404,
816
+ `role(s) not defined in this workspace: ${unknown.join(", ")}`,
817
+ );
626
818
 
627
819
  // Replace, in one transaction: a member must never be briefly role-less to a concurrent request.
628
820
  await this.db.transaction(async (tx) => {
@@ -630,8 +822,12 @@ export class RbacRepository {
630
822
  if (found.length) {
631
823
  await tx
632
824
  .insert(roleMember)
633
- .values(found.map((role) => ({ memberId: memberIdBig, roleId: role.id })))
634
- .onConflictDoNothing({ target: [roleMember.memberId, roleMember.roleId] });
825
+ .values(
826
+ found.map((role) => ({ memberId: memberIdBig, roleId: role.id })),
827
+ )
828
+ .onConflictDoNothing({
829
+ target: [roleMember.memberId, roleMember.roleId],
830
+ });
635
831
  }
636
832
  });
637
833
  await this.invalidateMemberById(memberIdBig);
@@ -647,37 +843,76 @@ export class RbacRepository {
647
843
  const rows = await this.db
648
844
  .select({ id: roles.id })
649
845
  .from(roles)
650
- .where(and(eq(roles.workspaceId, toId(workspaceId)), eq(roles.isDefault, true), eq(roles.isActive, true), eq(roles.isDeleted, false)));
846
+ .where(
847
+ and(
848
+ eq(roles.workspaceId, toId(workspaceId)),
849
+ eq(roles.isDefault, true),
850
+ eq(roles.isActive, true),
851
+ eq(roles.isDeleted, false),
852
+ ),
853
+ );
651
854
  return rows.map((role) => role.id);
652
855
  }
653
856
 
654
- async grantPermissionToMember(workspaceId: string, userId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
857
+ async grantPermissionToMember(
858
+ workspaceId: string,
859
+ userId: string,
860
+ permissionSlug: string,
861
+ actorUserId: string | null,
862
+ ): Promise<void> {
655
863
  const member = await this.requireMember(workspaceId, userId);
656
864
  const permission = await this.ensurePermission(permissionSlug, actorUserId);
657
865
  await this.db
658
866
  .insert(permissionMember)
659
867
  .values({ memberId: member.id, permissionId: permission.id })
660
- .onConflictDoNothing({ target: [permissionMember.memberId, permissionMember.permissionId] });
868
+ .onConflictDoNothing({
869
+ target: [permissionMember.memberId, permissionMember.permissionId],
870
+ });
661
871
  await this.invalidateMember(userId, workspaceId);
662
872
  }
663
873
 
664
- async revokePermissionFromMember(workspaceId: string, userId: string, permissionSlug: string): Promise<void> {
874
+ async revokePermissionFromMember(
875
+ workspaceId: string,
876
+ userId: string,
877
+ permissionSlug: string,
878
+ ): Promise<void> {
665
879
  const member = await this.requireMember(workspaceId, userId);
666
- const [permission] = await this.db.select({ id: permissions.id }).from(permissions).where(eq(permissions.slug, permissionSlug)).limit(1);
880
+ const [permission] = await this.db
881
+ .select({ id: permissions.id })
882
+ .from(permissions)
883
+ .where(eq(permissions.slug, permissionSlug))
884
+ .limit(1);
667
885
  if (!permission) return;
668
- await this.db.delete(permissionMember).where(and(eq(permissionMember.memberId, member.id), eq(permissionMember.permissionId, permission.id)));
886
+ await this.db
887
+ .delete(permissionMember)
888
+ .where(
889
+ and(
890
+ eq(permissionMember.memberId, member.id),
891
+ eq(permissionMember.permissionId, permission.id),
892
+ ),
893
+ );
669
894
  await this.invalidateMember(userId, workspaceId);
670
895
  }
671
896
 
672
897
  // Returns bigint directly: its one caller (assignRoleToMember) feeds the id straight into
673
898
  // another database call.
674
- private async requireRole(workspaceId: string, slug: string): Promise<{ id: bigint }> {
899
+ private async requireRole(
900
+ workspaceId: string,
901
+ slug: string,
902
+ ): Promise<{ id: bigint }> {
675
903
  const [role] = await this.db
676
904
  .select({ id: roles.id })
677
905
  .from(roles)
678
- .where(and(eq(roles.workspaceId, toId(workspaceId)), eq(roles.slug, slug), eq(roles.isDeleted, false)))
906
+ .where(
907
+ and(
908
+ eq(roles.workspaceId, toId(workspaceId)),
909
+ eq(roles.slug, slug),
910
+ eq(roles.isDeleted, false),
911
+ ),
912
+ )
679
913
  .limit(1);
680
- if (!role) throw new HttpError(404, `role "${slug}" not found in this workspace`);
914
+ if (!role)
915
+ throw new HttpError(404, `role "${slug}" not found in this workspace`);
681
916
  return role;
682
917
  }
683
918
 
@@ -685,13 +920,27 @@ export class RbacRepository {
685
920
  * Creates the row for a slug granted before anyone defined it; leaves an existing definition
686
921
  * alone. Returns bigint directly: both callers feed the id straight into another database call.
687
922
  */
688
- private async ensurePermission(slug: string, actorUserId: string | null): Promise<{ id: bigint }> {
689
- const [existing] = await this.db.select({ id: permissions.id }).from(permissions).where(eq(permissions.slug, slug)).limit(1);
923
+ private async ensurePermission(
924
+ slug: string,
925
+ actorUserId: string | null,
926
+ ): Promise<{ id: bigint }> {
927
+ const [existing] = await this.db
928
+ .select({ id: permissions.id })
929
+ .from(permissions)
930
+ .where(eq(permissions.slug, slug))
931
+ .limit(1);
690
932
  if (existing) return existing;
691
933
  const actorId = toIdOrNull(actorUserId);
692
934
  const [created] = await this.db
693
935
  .insert(permissions)
694
- .values({ slug, name: slug, displayName: slug, group: "Custom", createdBy: actorId, updatedBy: actorId })
936
+ .values({
937
+ slug,
938
+ name: slug,
939
+ displayName: slug,
940
+ group: "Custom",
941
+ createdBy: actorId,
942
+ updatedBy: actorId,
943
+ })
695
944
  .returning({ id: permissions.id });
696
945
  return created;
697
946
  }