@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
@@ -10,8 +10,14 @@ import {
10
10
  signAppleClientSecret,
11
11
  verifyIdTokenAndExtractProfile,
12
12
  } from "@/lib/auth/core/oauth.js";
13
- import { requestPasswordReset as coreRequestPasswordReset, resetPassword as coreResetPassword } from "@/lib/auth/core/password-reset.js";
14
- import { checkRateLimit, type RateLimitDeps } from "@/lib/auth/core/rate-limit.js";
13
+ import {
14
+ requestPasswordReset as coreRequestPasswordReset,
15
+ resetPassword as coreResetPassword,
16
+ } from "@/lib/auth/core/password-reset.js";
17
+ import {
18
+ checkRateLimit,
19
+ type RateLimitDeps,
20
+ } from "@/lib/auth/core/rate-limit.js";
15
21
  import {
16
22
  blockUser,
17
23
  createSession,
@@ -22,22 +28,47 @@ import {
22
28
  revokeSession,
23
29
  rotateRefreshToken,
24
30
  } from "@/lib/auth/core/session-policy.js";
25
- import { signAccessToken, signRefreshToken, signTwoFactorChallengeToken, verifyRefreshToken, verifyTwoFactorChallengeToken } from "@/lib/auth/core/token-service.js";
26
- import { buildTotpProvisioningUri, generateBackupCodes, generateTotpSecret, verifyTotpCode } from "@/lib/auth/core/two-factor.js";
31
+ import {
32
+ signAccessToken,
33
+ signRefreshToken,
34
+ signTwoFactorChallengeToken,
35
+ verifyRefreshToken,
36
+ verifyTwoFactorChallengeToken,
37
+ } from "@/lib/auth/core/token-service.js";
38
+ import {
39
+ buildTotpProvisioningUri,
40
+ generateBackupCodes,
41
+ generateTotpSecret,
42
+ verifyTotpCode,
43
+ } from "@/lib/auth/core/two-factor.js";
27
44
  import type { Revoker } from "@/lib/auth/core/types.js";
28
- import type { AuthConfig } from "./auth.config.js";
29
- import { HttpError } from "./http-error.js";
30
- import type { AuthzContext } from "./authz.middleware.js";
31
- import { AuditLogEntry, AuditLogListFilter, AuditLogRepository, toAuditLogEntry } from "./audit-log.repository.js";
32
- import type { Paginated } from "./pagination.js";
33
- import { PrismaClient } from "../generated/prisma/client.js";
34
- import { KeyProviderService } from "./key-provider.js";
35
- import { OAuthRepository } from "./oauth.repository.js";
36
- import { PasswordResetRepository } from "./password-reset.repository.js";
37
- import { MemberListFilter, MemberListResult, MemberSummary, PermissionInput, PermissionSummary, RbacRepository, RoleSummary, toMemberSummary } from "./rbac.repository.js";
38
- import { SessionRepository } from "./session.repository.js";
39
- import { TwoFactorRepository } from "./two-factor.repository.js";
40
- import { toId, toIdOrNull } from "./id.helper.js";
45
+ import type { AuthConfig } from "../auth.config.js";
46
+ import { HttpError } from "../http-error.js";
47
+ import type { AuthzContext } from "../middleware/authz.middleware.js";
48
+ import {
49
+ AuditLogEntry,
50
+ AuditLogListFilter,
51
+ AuditLogRepository,
52
+ toAuditLogEntry,
53
+ } from "../repositories/audit-log.repository.js";
54
+ import type { Paginated } from "../pagination.js";
55
+ import { PrismaClient } from "../../generated/prisma/client.js";
56
+ import { KeyProviderService } from "../key-provider.js";
57
+ import { OAuthRepository } from "../repositories/oauth.repository.js";
58
+ import { PasswordResetRepository } from "../repositories/password-reset.repository.js";
59
+ import {
60
+ MemberListFilter,
61
+ MemberListResult,
62
+ MemberSummary,
63
+ PermissionInput,
64
+ PermissionSummary,
65
+ RbacRepository,
66
+ RoleSummary,
67
+ toMemberSummary,
68
+ } from "../repositories/rbac.repository.js";
69
+ import { SessionRepository } from "../repositories/session.repository.js";
70
+ import { TwoFactorRepository } from "../repositories/two-factor.repository.js";
71
+ import { toId, toIdOrNull } from "../id.helper.js";
41
72
 
42
73
  export interface AuthTokens {
43
74
  accessToken: string;
@@ -92,7 +123,9 @@ export class AuthService {
92
123
  userAgent?: string;
93
124
  ip?: string;
94
125
  }): Promise<AuthTokens> {
95
- const existing = await this.prisma.user.findUnique({ where: { email: input.email } });
126
+ const existing = await this.prisma.user.findUnique({
127
+ where: { email: input.email },
128
+ });
96
129
  if (existing) throw new HttpError(409, "email already registered");
97
130
 
98
131
  const passwordHash = await hashPassword(input.password);
@@ -107,82 +140,174 @@ export class AuthService {
107
140
  username: input.username,
108
141
  },
109
142
  });
110
- return this.issueSessionTokens(user, { userAgent: input.userAgent, ip: input.ip });
143
+ return this.issueSessionTokens(user, {
144
+ userAgent: input.userAgent,
145
+ ip: input.ip,
146
+ });
111
147
  }
112
148
 
113
149
  /** Returns full tokens directly, or a short-lived challenge if the account has 2FA enabled — see `loginTwoFactor`. */
114
- async login(input: { identifier: string; password: string; userAgent?: string; ip?: string }): Promise<AuthTokens | TwoFactorChallenge> {
115
- const { allowed } = await checkRateLimit(this.rateLimit, "login", input.identifier);
150
+ async login(input: {
151
+ identifier: string;
152
+ password: string;
153
+ userAgent?: string;
154
+ ip?: string;
155
+ }): Promise<AuthTokens | TwoFactorChallenge> {
156
+ const { allowed } = await checkRateLimit(
157
+ this.rateLimit,
158
+ "login",
159
+ input.identifier,
160
+ );
116
161
  if (!allowed) throw new HttpError(429, "too many login attempts");
117
162
 
118
163
  const user = await this.prisma.user.findFirst({
119
- where: { OR: [{ email: input.identifier }, { username: input.identifier }, { phone: input.identifier }] },
164
+ where: {
165
+ OR: [
166
+ { email: input.identifier },
167
+ { username: input.identifier },
168
+ { phone: input.identifier },
169
+ ],
170
+ },
120
171
  });
121
- if (!user || user.blocked || !user.isActive || user.isDeleted || !user.passwordHash) throw new HttpError(401, "invalid credentials");
172
+ if (
173
+ !user ||
174
+ user.blocked ||
175
+ !user.isActive ||
176
+ user.isDeleted ||
177
+ !user.passwordHash
178
+ )
179
+ throw new HttpError(401, "invalid credentials");
122
180
 
123
181
  const valid = await verifyPassword(user.passwordHash, input.password);
124
182
  if (!valid) throw new HttpError(401, "invalid credentials");
125
183
 
126
184
  if (user.twoFactorEnabled) {
127
185
  const key = await this.keys.getActiveKey();
128
- const { token } = await signTwoFactorChallengeToken({ activeKey: key }, user.id.toString()); // core-facing: sub must be string
186
+ const { token } = await signTwoFactorChallengeToken(
187
+ { activeKey: key },
188
+ user.id.toString(),
189
+ ); // core-facing: sub must be string
129
190
  return { twoFactorRequired: true, challengeToken: token };
130
191
  }
131
192
 
132
- return this.issueSessionTokens(user, { userAgent: input.userAgent, ip: input.ip });
193
+ return this.issueSessionTokens(user, {
194
+ userAgent: input.userAgent,
195
+ ip: input.ip,
196
+ });
133
197
  }
134
198
 
135
199
  /** Completes the challenge `login()` returned when 2FA is enabled — a TOTP code or an unused backup code. */
136
- async loginTwoFactor(input: { challengeToken: string; code: string; userAgent?: string; ip?: string }): Promise<AuthTokens> {
137
- const { sub } = await verifyTwoFactorChallengeToken({ secret: this.keys.secret }, input.challengeToken);
200
+ async loginTwoFactor(input: {
201
+ challengeToken: string;
202
+ code: string;
203
+ userAgent?: string;
204
+ ip?: string;
205
+ }): Promise<AuthTokens> {
206
+ const { sub } = await verifyTwoFactorChallengeToken(
207
+ { secret: this.keys.secret },
208
+ input.challengeToken,
209
+ );
138
210
 
139
- const user = await this.prisma.user.findUnique({ where: { id: toId(sub) } });
140
- if (!user || user.blocked || !user.isActive || user.isDeleted || !user.twoFactorEnabled || !user.twoFactorSecret) throw new HttpError(401, "invalid credentials");
211
+ const user = await this.prisma.user.findUnique({
212
+ where: { id: toId(sub) },
213
+ });
214
+ if (
215
+ !user ||
216
+ user.blocked ||
217
+ !user.isActive ||
218
+ user.isDeleted ||
219
+ !user.twoFactorEnabled ||
220
+ !user.twoFactorSecret
221
+ )
222
+ throw new HttpError(401, "invalid credentials");
141
223
 
142
224
  const validTotp = verifyTotpCode(user.twoFactorSecret, input.code);
143
- const validBackup = !validTotp && (await this.twoFactor.consumeBackupCode(user.id, input.code));
225
+ const validBackup =
226
+ !validTotp &&
227
+ (await this.twoFactor.consumeBackupCode(user.id, input.code));
144
228
  if (!validTotp && !validBackup) {
145
- await this.sessions.appendAuditEvent({ type: "two_factor_challenge_failed", userId: user.id.toString() }); // core-facing: string
229
+ await this.sessions.appendAuditEvent({
230
+ type: "two_factor_challenge_failed",
231
+ userId: user.id.toString(),
232
+ }); // core-facing: string
146
233
  throw new HttpError(401, "invalid two-factor code");
147
234
  }
148
235
 
149
- return this.issueSessionTokens(user, { userAgent: input.userAgent, ip: input.ip });
236
+ return this.issueSessionTokens(user, {
237
+ userAgent: input.userAgent,
238
+ ip: input.ip,
239
+ });
150
240
  }
151
241
 
152
- async enrollTwoFactor(userId: string): Promise<{ secret: string; provisioningUri: string }> {
242
+ async enrollTwoFactor(
243
+ userId: string,
244
+ ): Promise<{ secret: string; provisioningUri: string }> {
153
245
  const idBig = toId(userId);
154
246
  const user = await this.getUserOrThrow(userId);
155
247
  const secret = generateTotpSecret();
156
248
  // Not enabled yet — confirmTwoFactor() flips that, so an abandoned enrollment never locks the account out.
157
- await this.prisma.user.update({ where: { id: idBig }, data: { twoFactorSecret: secret, twoFactorEnabled: false } });
158
- return { secret, provisioningUri: buildTotpProvisioningUri({ secret, accountName: user.email, issuer: this.config.twoFactorIssuer }) };
249
+ await this.prisma.user.update({
250
+ where: { id: idBig },
251
+ data: { twoFactorSecret: secret, twoFactorEnabled: false },
252
+ });
253
+ return {
254
+ secret,
255
+ provisioningUri: buildTotpProvisioningUri({
256
+ secret,
257
+ accountName: user.email,
258
+ issuer: this.config.twoFactorIssuer,
259
+ }),
260
+ };
159
261
  }
160
262
 
161
- async confirmTwoFactor(userId: string, code: string): Promise<{ backupCodes: string[] }> {
263
+ async confirmTwoFactor(
264
+ userId: string,
265
+ code: string,
266
+ ): Promise<{ backupCodes: string[] }> {
162
267
  const idBig = toId(userId);
163
268
  const user = await this.getUserOrThrow(userId);
164
- if (!user.twoFactorSecret) throw new HttpError(400, "call enrollTwoFactor first");
165
- if (!verifyTotpCode(user.twoFactorSecret, code)) throw new HttpError(401, "invalid two-factor code");
269
+ if (!user.twoFactorSecret)
270
+ throw new HttpError(400, "call enrollTwoFactor first");
271
+ if (!verifyTotpCode(user.twoFactorSecret, code))
272
+ throw new HttpError(401, "invalid two-factor code");
166
273
 
167
274
  const backupCodes = generateBackupCodes();
168
- await this.twoFactor.saveBackupCodes(idBig, backupCodes.map((c) => c.hash));
169
- await this.prisma.user.update({ where: { id: idBig }, data: { twoFactorEnabled: true } });
170
- await this.sessions.appendAuditEvent({ type: "two_factor_enabled", userId });
275
+ await this.twoFactor.saveBackupCodes(
276
+ idBig,
277
+ backupCodes.map((c) => c.hash),
278
+ );
279
+ await this.prisma.user.update({
280
+ where: { id: idBig },
281
+ data: { twoFactorEnabled: true },
282
+ });
283
+ await this.sessions.appendAuditEvent({
284
+ type: "two_factor_enabled",
285
+ userId,
286
+ });
171
287
  return { backupCodes: backupCodes.map((c) => c.code) };
172
288
  }
173
289
 
174
290
  async disableTwoFactor(userId: string, code: string): Promise<void> {
175
291
  const idBig = toId(userId);
176
292
  const user = await this.getUserOrThrow(userId);
177
- if (!user.twoFactorEnabled || !user.twoFactorSecret) throw new HttpError(400, "two-factor is not enabled");
293
+ if (!user.twoFactorEnabled || !user.twoFactorSecret)
294
+ throw new HttpError(400, "two-factor is not enabled");
178
295
 
179
296
  const validTotp = verifyTotpCode(user.twoFactorSecret, code);
180
- const validBackup = !validTotp && (await this.twoFactor.consumeBackupCode(idBig, code));
181
- if (!validTotp && !validBackup) throw new HttpError(401, "invalid two-factor code");
297
+ const validBackup =
298
+ !validTotp && (await this.twoFactor.consumeBackupCode(idBig, code));
299
+ if (!validTotp && !validBackup)
300
+ throw new HttpError(401, "invalid two-factor code");
182
301
 
183
- await this.prisma.user.update({ where: { id: idBig }, data: { twoFactorEnabled: false, twoFactorSecret: null } });
302
+ await this.prisma.user.update({
303
+ where: { id: idBig },
304
+ data: { twoFactorEnabled: false, twoFactorSecret: null },
305
+ });
184
306
  await this.twoFactor.clearBackupCodes(idBig);
185
- await this.sessions.appendAuditEvent({ type: "two_factor_disabled", userId });
307
+ await this.sessions.appendAuditEvent({
308
+ type: "two_factor_disabled",
309
+ userId,
310
+ });
186
311
  }
187
312
 
188
313
  /** Always succeeds from the caller's point of view — an unknown email or a throttled request looks identical, to avoid enumeration. */
@@ -190,15 +315,27 @@ export class AuthService {
190
315
  const user = await this.prisma.user.findUnique({ where: { email } });
191
316
  if (!user || user.isDeleted) return;
192
317
 
193
- const { allowed } = await checkRateLimit(this.rateLimit, "password-reset", email);
318
+ const { allowed } = await checkRateLimit(
319
+ this.rateLimit,
320
+ "password-reset",
321
+ email,
322
+ );
194
323
  if (!allowed) return;
195
324
 
196
- const { token } = await coreRequestPasswordReset(this.passwordReset, user.id.toString());
197
- if (this.config.sendPasswordResetEmail) await this.config.sendPasswordResetEmail(email, token);
325
+ const { token } = await coreRequestPasswordReset(
326
+ this.passwordReset,
327
+ user.id.toString(),
328
+ );
329
+ if (this.config.sendPasswordResetEmail)
330
+ await this.config.sendPasswordResetEmail(email, token);
198
331
  }
199
332
 
200
333
  async resetPassword(token: string, newPassword: string): Promise<void> {
201
- const { userId } = await coreResetPassword(this.passwordReset, token, newPassword);
334
+ const { userId } = await coreResetPassword(
335
+ this.passwordReset,
336
+ token,
337
+ newPassword,
338
+ );
202
339
  // The old password is no longer trusted, so neither are its sessions. The revoker is the user
203
340
  // themselves — they proved control of the account by consuming the reset token.
204
341
  await revokeAllSessionsForUser(this.sessions, userId, { userId });
@@ -206,22 +343,40 @@ export class AuthService {
206
343
 
207
344
  /** Builds the redirect URL for `provider`. `state` is an opaque anti-CSRF nonce the provider echoes back verbatim. */
208
345
  async startOAuth(provider: string): Promise<{ url: string }> {
209
- const state = Buffer.from(JSON.stringify({ nonce: randomUUID() })).toString("base64url");
346
+ const state = Buffer.from(JSON.stringify({ nonce: randomUUID() })).toString(
347
+ "base64url",
348
+ );
210
349
 
211
350
  if (provider === "google") {
212
351
  const creds = this.config.oauthProviders.google;
213
352
  if (!creds) throw new HttpError(400, "google OAuth is not configured");
214
- return { url: buildAuthorizationUrl(GOOGLE_OIDC_PROVIDER, { clientId: creds.clientId, redirectUri: creds.redirectUri, state }) };
353
+ return {
354
+ url: buildAuthorizationUrl(GOOGLE_OIDC_PROVIDER, {
355
+ clientId: creds.clientId,
356
+ redirectUri: creds.redirectUri,
357
+ state,
358
+ }),
359
+ };
215
360
  }
216
361
  if (provider === "apple") {
217
362
  const creds = this.config.oauthProviders.apple;
218
363
  if (!creds) throw new HttpError(400, "apple OAuth is not configured");
219
- return { url: buildAuthorizationUrl(APPLE_OIDC_PROVIDER, { clientId: creds.clientId, redirectUri: creds.redirectUri, state }) };
364
+ return {
365
+ url: buildAuthorizationUrl(APPLE_OIDC_PROVIDER, {
366
+ clientId: creds.clientId,
367
+ redirectUri: creds.redirectUri,
368
+ state,
369
+ }),
370
+ };
220
371
  }
221
372
  throw new HttpError(400, `unknown OAuth provider "${provider}"`);
222
373
  }
223
374
 
224
- async completeOAuthCallback(provider: string, code: string, _state: string): Promise<AuthTokens> {
375
+ async completeOAuthCallback(
376
+ provider: string,
377
+ code: string,
378
+ _state: string,
379
+ ): Promise<AuthTokens> {
225
380
  let providerDescriptor: OAuthProviderDescriptor;
226
381
  let clientId: string;
227
382
  let idToken: string;
@@ -232,44 +387,91 @@ export class AuthService {
232
387
  providerDescriptor = GOOGLE_OIDC_PROVIDER;
233
388
  clientId = creds.clientId;
234
389
  idToken = (
235
- await exchangeCodeForTokens(GOOGLE_OIDC_PROVIDER, { clientId: creds.clientId, clientSecret: creds.clientSecret, redirectUri: creds.redirectUri, code })
390
+ await exchangeCodeForTokens(GOOGLE_OIDC_PROVIDER, {
391
+ clientId: creds.clientId,
392
+ clientSecret: creds.clientSecret,
393
+ redirectUri: creds.redirectUri,
394
+ code,
395
+ })
236
396
  ).idToken;
237
397
  } else if (provider === "apple") {
238
398
  const creds = this.config.oauthProviders.apple;
239
399
  if (!creds) throw new HttpError(400, "apple OAuth is not configured");
240
400
  providerDescriptor = APPLE_OIDC_PROVIDER;
241
401
  clientId = creds.clientId;
242
- const clientSecret = await signAppleClientSecret({ teamId: creds.teamId, clientId: creds.clientId, keyId: creds.keyId, privateKeyPem: creds.privateKey });
243
- idToken = (await exchangeCodeForTokens(APPLE_OIDC_PROVIDER, { clientId: creds.clientId, clientSecret, redirectUri: creds.redirectUri, code })).idToken;
402
+ const clientSecret = await signAppleClientSecret({
403
+ teamId: creds.teamId,
404
+ clientId: creds.clientId,
405
+ keyId: creds.keyId,
406
+ privateKeyPem: creds.privateKey,
407
+ });
408
+ idToken = (
409
+ await exchangeCodeForTokens(APPLE_OIDC_PROVIDER, {
410
+ clientId: creds.clientId,
411
+ clientSecret,
412
+ redirectUri: creds.redirectUri,
413
+ code,
414
+ })
415
+ ).idToken;
244
416
  } else {
245
417
  throw new HttpError(400, `unknown OAuth provider "${provider}"`);
246
418
  }
247
419
 
248
- const profile = await verifyIdTokenAndExtractProfile(providerDescriptor, { clientId, idToken });
249
- const { userId } = await completeOAuthLogin(this.oauth, { provider, profile });
420
+ const profile = await verifyIdTokenAndExtractProfile(providerDescriptor, {
421
+ clientId,
422
+ idToken,
423
+ });
424
+ const { userId } = await completeOAuthLogin(this.oauth, {
425
+ provider,
426
+ profile,
427
+ });
250
428
 
251
429
  const user = await this.getUserOrThrow(userId);
252
- if (user.blocked || !user.isActive || user.isDeleted) throw new HttpError(401, "account is blocked");
430
+ if (user.blocked || !user.isActive || user.isDeleted)
431
+ throw new HttpError(401, "account is blocked");
253
432
  return this.issueSessionTokens(user, { provider });
254
433
  }
255
434
 
256
435
  /** Pinned to the caller's workspace — the filter argument cannot widen it. */
257
- async listAuditLog(ctx: AuthzContext, filter: AuditLogListFilter): Promise<Paginated<AuditLogEntry>> {
258
- const { items, meta } = await this.auditLog.list({ ...filter, workspaceId: ctx.workspaceId });
436
+ async listAuditLog(
437
+ ctx: AuthzContext,
438
+ filter: AuditLogListFilter,
439
+ ): Promise<Paginated<AuditLogEntry>> {
440
+ const { items, meta } = await this.auditLog.list({
441
+ ...filter,
442
+ workspaceId: ctx.workspaceId,
443
+ });
259
444
  return { items: items.map(toAuditLogEntry), meta };
260
445
  }
261
446
 
262
447
  /** `req.auth` (the JWT claims) never carries 2FA status, so `/auth/me` fetches it fresh — the one bit of the response that isn't just echoing the token. */
263
- async getTwoFactorStatus(userId: string): Promise<{ twoFactorEnabled: boolean }> {
448
+ async getTwoFactorStatus(
449
+ userId: string,
450
+ ): Promise<{ twoFactorEnabled: boolean }> {
264
451
  const user = await this.getUserOrThrow(userId);
265
452
  return { twoFactorEnabled: user.twoFactorEnabled };
266
453
  }
267
454
 
268
- async listActiveSessions(userId: string): Promise<Array<{ id: string; createdAt: string; expiresAt: string; provider?: string; userAgent?: string; ip?: string }>> {
455
+ async listActiveSessions(
456
+ userId: string,
457
+ ): Promise<
458
+ Array<{
459
+ id: string;
460
+ createdAt: string;
461
+ expiresAt: string;
462
+ provider?: string;
463
+ userAgent?: string;
464
+ ip?: string;
465
+ }>
466
+ > {
269
467
  // "Active" is now three conditions, not one: not revoked, and not past its own absolute
270
468
  // expiry. `sessions_user_active_idx` is (user_id, is_revoked, expires_at) for exactly this read.
271
469
  const rows = await this.prisma.session.findMany({
272
- where: { userId: toId(userId), isRevoked: false, expiresAt: { gt: new Date() } },
470
+ where: {
471
+ userId: toId(userId),
472
+ isRevoked: false,
473
+ expiresAt: { gt: new Date() },
474
+ },
273
475
  orderBy: { createdAt: "desc" },
274
476
  });
275
477
  return rows.map((row) => ({
@@ -288,8 +490,14 @@ export class AuthService {
288
490
  // database through a workspace-scoped query. That is the security property this model rests on:
289
491
  // an admin of one workspace has no expressible way to name a row in another.
290
492
 
291
- async listUsers(ctx: AuthzContext, filter: MemberListFilter): Promise<MemberListResult> {
292
- const { items, meta } = await this.rbac.listMembers(ctx.workspaceId, filter);
493
+ async listUsers(
494
+ ctx: AuthzContext,
495
+ filter: MemberListFilter,
496
+ ): Promise<MemberListResult> {
497
+ const { items, meta } = await this.rbac.listMembers(
498
+ ctx.workspaceId,
499
+ filter,
500
+ );
293
501
  return { items: items.map(toMemberSummary), meta };
294
502
  }
295
503
 
@@ -300,13 +508,25 @@ export class AuthService {
300
508
  async updateUser(
301
509
  ctx: AuthzContext,
302
510
  userId: string,
303
- input: { firstName?: string | null; lastName?: string | null; displayName?: string | null; phone?: string | null; username?: string | null; photo?: string | null },
511
+ input: {
512
+ firstName?: string | null;
513
+ lastName?: string | null;
514
+ displayName?: string | null;
515
+ phone?: string | null;
516
+ username?: string | null;
517
+ photo?: string | null;
518
+ },
304
519
  actorUserId: string | null,
305
520
  ): Promise<MemberSummary> {
306
521
  return this.rbac.updateMember(ctx.workspaceId, userId, input, actorUserId);
307
522
  }
308
523
 
309
- async deleteUser(ctx: AuthzContext, userId: string, actorUserId: string | null, reason?: string): Promise<void> {
524
+ async deleteUser(
525
+ ctx: AuthzContext,
526
+ userId: string,
527
+ actorUserId: string | null,
528
+ reason?: string,
529
+ ): Promise<void> {
310
530
  await this.rbac.deleteMember(ctx.workspaceId, userId, actorUserId, reason);
311
531
  }
312
532
 
@@ -319,7 +539,10 @@ export class AuthService {
319
539
  }
320
540
 
321
541
  /** Defines a permission, or edits one — including deactivating it. The write path for "the catalog is editable in the database". */
322
- async definePermission(input: PermissionInput, actorUserId: string | null): Promise<PermissionSummary> {
542
+ async definePermission(
543
+ input: PermissionInput,
544
+ actorUserId: string | null,
545
+ ): Promise<PermissionSummary> {
323
546
  return this.rbac.upsertPermission(input, actorUserId);
324
547
  }
325
548
 
@@ -329,7 +552,12 @@ export class AuthService {
329
552
 
330
553
  async createRole(
331
554
  ctx: AuthzContext,
332
- input: { slug: string; name?: string; displayName?: string; description?: string | null },
555
+ input: {
556
+ slug: string;
557
+ name?: string;
558
+ displayName?: string;
559
+ description?: string | null;
560
+ },
333
561
  actorUserId: string | null,
334
562
  ): Promise<RoleSummary> {
335
563
  return this.rbac.createRole(ctx.workspaceId, input, actorUserId);
@@ -338,47 +566,114 @@ export class AuthService {
338
566
  async updateRole(
339
567
  ctx: AuthzContext,
340
568
  roleId: string,
341
- input: { name?: string; displayName?: string; description?: string | null; isActive?: boolean },
569
+ input: {
570
+ name?: string;
571
+ displayName?: string;
572
+ description?: string | null;
573
+ isActive?: boolean;
574
+ },
342
575
  actorUserId: string | null,
343
576
  ): Promise<RoleSummary> {
344
577
  return this.rbac.updateRole(ctx.workspaceId, roleId, input, actorUserId);
345
578
  }
346
579
 
347
- async deleteRole(ctx: AuthzContext, roleId: string, actorUserId: string | null, reason?: string): Promise<void> {
580
+ async deleteRole(
581
+ ctx: AuthzContext,
582
+ roleId: string,
583
+ actorUserId: string | null,
584
+ reason?: string,
585
+ ): Promise<void> {
348
586
  await this.rbac.deleteRole(ctx.workspaceId, roleId, actorUserId, reason);
349
587
  }
350
588
 
351
- async attachPermissionToRole(ctx: AuthzContext, roleId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
352
- await this.rbac.attachPermissionToRole(ctx.workspaceId, roleId, permissionSlug, actorUserId);
589
+ async attachPermissionToRole(
590
+ ctx: AuthzContext,
591
+ roleId: string,
592
+ permissionSlug: string,
593
+ actorUserId: string | null,
594
+ ): Promise<void> {
595
+ await this.rbac.attachPermissionToRole(
596
+ ctx.workspaceId,
597
+ roleId,
598
+ permissionSlug,
599
+ actorUserId,
600
+ );
353
601
  }
354
602
 
355
- async assignRole(ctx: AuthzContext, userId: string, roleSlug: string): Promise<void> {
603
+ async assignRole(
604
+ ctx: AuthzContext,
605
+ userId: string,
606
+ roleSlug: string,
607
+ ): Promise<void> {
356
608
  await this.rbac.assignRoleToMember(ctx.workspaceId, userId, roleSlug);
357
- await this.auditLog.append({ type: "role_assigned", userId, role: roleSlug }, { workspaceId: ctx.workspaceId });
609
+ await this.auditLog.append(
610
+ { type: "role_assigned", userId, role: roleSlug },
611
+ { workspaceId: ctx.workspaceId },
612
+ );
358
613
  }
359
614
 
360
- async revokeRole(ctx: AuthzContext, userId: string, roleSlug: string): Promise<void> {
615
+ async revokeRole(
616
+ ctx: AuthzContext,
617
+ userId: string,
618
+ roleSlug: string,
619
+ ): Promise<void> {
361
620
  await this.rbac.revokeRoleFromMember(ctx.workspaceId, userId, roleSlug);
362
- await this.auditLog.append({ type: "role_revoked", userId, role: roleSlug }, { workspaceId: ctx.workspaceId });
621
+ await this.auditLog.append(
622
+ { type: "role_revoked", userId, role: roleSlug },
623
+ { workspaceId: ctx.workspaceId },
624
+ );
363
625
  }
364
626
 
365
- async grantPermission(ctx: AuthzContext, userId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
366
- await this.rbac.grantPermissionToMember(ctx.workspaceId, userId, permissionSlug, actorUserId);
367
- await this.auditLog.append({ type: "permission_granted", userId, permission: permissionSlug }, { workspaceId: ctx.workspaceId });
627
+ async grantPermission(
628
+ ctx: AuthzContext,
629
+ userId: string,
630
+ permissionSlug: string,
631
+ actorUserId: string | null,
632
+ ): Promise<void> {
633
+ await this.rbac.grantPermissionToMember(
634
+ ctx.workspaceId,
635
+ userId,
636
+ permissionSlug,
637
+ actorUserId,
638
+ );
639
+ await this.auditLog.append(
640
+ { type: "permission_granted", userId, permission: permissionSlug },
641
+ { workspaceId: ctx.workspaceId },
642
+ );
368
643
  }
369
644
 
370
- async revokePermission(ctx: AuthzContext, userId: string, permissionSlug: string): Promise<void> {
371
- await this.rbac.revokePermissionFromMember(ctx.workspaceId, userId, permissionSlug);
372
- await this.auditLog.append({ type: "permission_revoked", userId, permission: permissionSlug }, { workspaceId: ctx.workspaceId });
645
+ async revokePermission(
646
+ ctx: AuthzContext,
647
+ userId: string,
648
+ permissionSlug: string,
649
+ ): Promise<void> {
650
+ await this.rbac.revokePermissionFromMember(
651
+ ctx.workspaceId,
652
+ userId,
653
+ permissionSlug,
654
+ );
655
+ await this.auditLog.append(
656
+ { type: "permission_revoked", userId, permission: permissionSlug },
657
+ { workspaceId: ctx.workspaceId },
658
+ );
373
659
  }
374
660
 
375
661
  async refresh(refreshToken: string): Promise<Omit<AuthTokens, "sessionId">> {
376
662
  const key = await this.keys.getActiveKey();
377
- const presented = await verifyRefreshToken({ secret: this.keys.secret }, refreshToken);
378
- const { session, nextJti } = await rotateRefreshToken(this.sessions, presented);
663
+ const presented = await verifyRefreshToken(
664
+ { secret: this.keys.secret },
665
+ refreshToken,
666
+ );
667
+ const { session, nextJti } = await rotateRefreshToken(
668
+ this.sessions,
669
+ presented,
670
+ );
379
671
 
380
- const user = await this.prisma.user.findUnique({ where: { id: toId(session.userId) } });
381
- if (!user || user.blocked || !user.isActive || user.isDeleted) throw new HttpError(401, "invalid credentials");
672
+ const user = await this.prisma.user.findUnique({
673
+ where: { id: toId(session.userId) },
674
+ });
675
+ if (!user || user.blocked || !user.isActive || user.isDeleted)
676
+ throw new HttpError(401, "invalid credentials");
382
677
 
383
678
  const access = await signAccessToken(
384
679
  { activeKey: key },
@@ -387,38 +682,81 @@ export class AuthService {
387
682
  );
388
683
  const refresh = await signRefreshToken(
389
684
  { activeKey: key },
390
- { sub: user.id.toString(), sessionId: session.id, sv: session.sessionVersion },
685
+ {
686
+ sub: user.id.toString(),
687
+ sessionId: session.id,
688
+ sv: session.sessionVersion,
689
+ },
391
690
  { jti: nextJti, ttlSeconds: this.config.refreshTokenTtlSeconds },
392
691
  );
393
692
  return { accessToken: access.token, refreshToken: refresh.token };
394
693
  }
395
694
 
396
- async logout(sessionId: string, accessJti: string, accessRemainingTtlSeconds: number, revoker?: Revoker): Promise<void> {
695
+ async logout(
696
+ sessionId: string,
697
+ accessJti: string,
698
+ accessRemainingTtlSeconds: number,
699
+ revoker?: Revoker,
700
+ ): Promise<void> {
397
701
  await revokeSession(this.sessions, sessionId, revoker);
398
- await revokeAccessToken(this.sessions, accessJti, accessRemainingTtlSeconds);
702
+ await revokeAccessToken(
703
+ this.sessions,
704
+ accessJti,
705
+ accessRemainingTtlSeconds,
706
+ );
399
707
  }
400
708
 
401
709
  async logoutAll(userId: string, revoker?: Revoker): Promise<void> {
402
710
  await revokeAllSessionsForUser(this.sessions, userId, revoker);
403
711
  }
404
712
 
405
- async logoutOthers(userId: string, keepSessionId: string, revoker?: Revoker): Promise<void> {
406
- await revokeOtherSessionsForUser(this.sessions, userId, keepSessionId, revoker);
713
+ async logoutOthers(
714
+ userId: string,
715
+ keepSessionId: string,
716
+ revoker?: Revoker,
717
+ ): Promise<void> {
718
+ await revokeOtherSessionsForUser(
719
+ this.sessions,
720
+ userId,
721
+ keepSessionId,
722
+ revoker,
723
+ );
407
724
  }
408
725
 
409
- async changePassword(userId: string, currentSessionId: string, input: { currentPassword: string; newPassword: string }): Promise<void> {
726
+ async changePassword(
727
+ userId: string,
728
+ currentSessionId: string,
729
+ input: { currentPassword: string; newPassword: string },
730
+ ): Promise<void> {
410
731
  const userIdBig = toId(userId);
411
- const user = await this.prisma.user.findUnique({ where: { id: userIdBig } });
412
- if (!user || user.blocked || !user.isActive || user.isDeleted || !user.passwordHash) throw new HttpError(401, "invalid credentials");
413
-
414
- const valid = await verifyPassword(user.passwordHash, input.currentPassword);
732
+ const user = await this.prisma.user.findUnique({
733
+ where: { id: userIdBig },
734
+ });
735
+ if (
736
+ !user ||
737
+ user.blocked ||
738
+ !user.isActive ||
739
+ user.isDeleted ||
740
+ !user.passwordHash
741
+ )
742
+ throw new HttpError(401, "invalid credentials");
743
+
744
+ const valid = await verifyPassword(
745
+ user.passwordHash,
746
+ input.currentPassword,
747
+ );
415
748
  if (!valid) throw new HttpError(401, "invalid credentials");
416
749
 
417
750
  const passwordHash = await hashPassword(input.newPassword);
418
- await this.prisma.user.update({ where: { id: userIdBig }, data: { passwordHash, updatedBy: userIdBig } });
751
+ await this.prisma.user.update({
752
+ where: { id: userIdBig },
753
+ data: { passwordHash, updatedBy: userIdBig },
754
+ });
419
755
  // The old password is no longer trusted everywhere else it's signed in — but leave the
420
756
  // session making this very call alone, the same courtesy `logoutOthers` extends.
421
- await revokeOtherSessionsForUser(this.sessions, userId, currentSessionId, { userId });
757
+ await revokeOtherSessionsForUser(this.sessions, userId, currentSessionId, {
758
+ userId,
759
+ });
422
760
  }
423
761
 
424
762
  /** Backs both `GET`- and `PATCH /auth/me` — deliberately not workspace-scoped, see `updateProfile`. */
@@ -449,7 +787,9 @@ export class AuthService {
449
787
  /** Self-service — no `users:manage` permission required, callable by anyone on their own row, and deliberately not workspace-scoped, see `updateProfile`. */
450
788
  async getProfile(userId: string): Promise<SelfProfile> {
451
789
  const userIdBig = toId(userId);
452
- const user = await this.prisma.user.findUnique({ where: { id: userIdBig, isDeleted: false } });
790
+ const user = await this.prisma.user.findUnique({
791
+ where: { id: userIdBig, isDeleted: false },
792
+ });
453
793
  if (!user) throw new HttpError(401, "invalid credentials");
454
794
  return this.toSelfProfile(user);
455
795
  }
@@ -462,12 +802,24 @@ export class AuthService {
462
802
  */
463
803
  async updateProfile(
464
804
  userId: string,
465
- input: { firstName?: string | null; lastName?: string | null; displayName?: string | null; phone?: string | null; username?: string | null; photo?: string | null },
805
+ input: {
806
+ firstName?: string | null;
807
+ lastName?: string | null;
808
+ displayName?: string | null;
809
+ phone?: string | null;
810
+ username?: string | null;
811
+ photo?: string | null;
812
+ },
466
813
  ): Promise<SelfProfile> {
467
814
  const userIdBig = toId(userId);
468
- const existing = await this.prisma.user.findUnique({ where: { id: userIdBig, isDeleted: false } });
815
+ const existing = await this.prisma.user.findUnique({
816
+ where: { id: userIdBig, isDeleted: false },
817
+ });
469
818
  if (!existing) throw new HttpError(401, "invalid credentials");
470
- const user = await this.prisma.user.update({ where: { id: userIdBig }, data: { ...input, updatedBy: userIdBig } });
819
+ const user = await this.prisma.user.update({
820
+ where: { id: userIdBig },
821
+ data: { ...input, updatedBy: userIdBig },
822
+ });
471
823
  return this.toSelfProfile(user);
472
824
  }
473
825
 
@@ -476,9 +828,16 @@ export class AuthService {
476
828
  * caller's workspace — otherwise an admin of one workspace could disable an account they
477
829
  * have no relationship with.
478
830
  */
479
- async block(ctx: AuthzContext, userId: string, revoker?: Revoker): Promise<void> {
831
+ async block(
832
+ ctx: AuthzContext,
833
+ userId: string,
834
+ revoker?: Revoker,
835
+ ): Promise<void> {
480
836
  await this.rbac.requireMember(ctx.workspaceId, userId);
481
- await this.prisma.user.update({ where: { id: toId(userId) }, data: { blocked: true, updatedBy: toIdOrNull(revoker?.userId) } });
837
+ await this.prisma.user.update({
838
+ where: { id: toId(userId) },
839
+ data: { blocked: true, updatedBy: toIdOrNull(revoker?.userId) },
840
+ });
482
841
  // The administrator, not the blocked user, is what lands in `sessions.revoked_by`.
483
842
  await blockUser(this.sessions, userId, revoker);
484
843
  // Belt and braces. The block is enforced on the authentication path — login and refresh both
@@ -488,30 +847,53 @@ export class AuthService {
488
847
  await this.rbac.invalidateMember(userId, ctx.workspaceId);
489
848
  }
490
849
 
491
- async unblock(ctx: AuthzContext, userId: string, revoker?: Revoker): Promise<void> {
850
+ async unblock(
851
+ ctx: AuthzContext,
852
+ userId: string,
853
+ revoker?: Revoker,
854
+ ): Promise<void> {
492
855
  await this.rbac.requireMember(ctx.workspaceId, userId);
493
- await this.prisma.user.update({ where: { id: toId(userId) }, data: { blocked: false, updatedBy: toIdOrNull(revoker?.userId) } });
856
+ await this.prisma.user.update({
857
+ where: { id: toId(userId) },
858
+ data: { blocked: false, updatedBy: toIdOrNull(revoker?.userId) },
859
+ });
494
860
  }
495
861
 
496
862
  /**
497
863
  * A routine administrative on/off toggle — distinct from `block`/`unblock`, which is a
498
864
  * security/moderation action. Both independently deny login; see the note on the `User` model.
499
865
  */
500
- async deactivate(ctx: AuthzContext, userId: string, revoker?: Revoker): Promise<void> {
866
+ async deactivate(
867
+ ctx: AuthzContext,
868
+ userId: string,
869
+ revoker?: Revoker,
870
+ ): Promise<void> {
501
871
  await this.rbac.requireMember(ctx.workspaceId, userId);
502
- await this.prisma.user.update({ where: { id: toId(userId) }, data: { isActive: false, updatedBy: toIdOrNull(revoker?.userId) } });
872
+ await this.prisma.user.update({
873
+ where: { id: toId(userId) },
874
+ data: { isActive: false, updatedBy: toIdOrNull(revoker?.userId) },
875
+ });
503
876
  await deactivateUser(this.sessions, userId, revoker);
504
877
  await this.rbac.invalidateMember(userId, ctx.workspaceId);
505
878
  }
506
879
 
507
- async activate(ctx: AuthzContext, userId: string, revoker?: Revoker): Promise<void> {
880
+ async activate(
881
+ ctx: AuthzContext,
882
+ userId: string,
883
+ revoker?: Revoker,
884
+ ): Promise<void> {
508
885
  await this.rbac.requireMember(ctx.workspaceId, userId);
509
- await this.prisma.user.update({ where: { id: toId(userId) }, data: { isActive: true, updatedBy: toIdOrNull(revoker?.userId) } });
886
+ await this.prisma.user.update({
887
+ where: { id: toId(userId) },
888
+ data: { isActive: true, updatedBy: toIdOrNull(revoker?.userId) },
889
+ });
510
890
  }
511
891
 
512
892
  /** Nest's filter turns Prisma's `findUniqueOrThrow` into a 404; without one, this does it by hand. */
513
893
  private async getUserOrThrow(userId: string) {
514
- const user = await this.prisma.user.findUnique({ where: { id: toId(userId) } });
894
+ const user = await this.prisma.user.findUnique({
895
+ where: { id: toId(userId) },
896
+ });
515
897
  if (!user) throw new HttpError(404, `user "${userId}" not found`);
516
898
  return user;
517
899
  }
@@ -522,8 +904,14 @@ export class AuthService {
522
904
  * Each request resolves its own — see AuthzGuard. Takes the raw Prisma user row (bigint id) —
523
905
  * every call site already has one in hand.
524
906
  */
525
- private async issueSessionTokens(user: { id: bigint }, meta: { userAgent?: string; ip?: string; provider?: string }): Promise<AuthTokens> {
526
- const session = await createSession(this.sessions, { userId: user.id.toString(), ...meta });
907
+ private async issueSessionTokens(
908
+ user: { id: bigint },
909
+ meta: { userAgent?: string; ip?: string; provider?: string },
910
+ ): Promise<AuthTokens> {
911
+ const session = await createSession(this.sessions, {
912
+ userId: user.id.toString(),
913
+ ...meta,
914
+ });
527
915
  const key = await this.keys.getActiveKey();
528
916
 
529
917
  const access = await signAccessToken(
@@ -533,10 +921,21 @@ export class AuthService {
533
921
  );
534
922
  const refresh = await signRefreshToken(
535
923
  { activeKey: key },
536
- { sub: user.id.toString(), sessionId: session.id, sv: session.sessionVersion },
537
- { jti: session.currentRefreshJti, ttlSeconds: this.config.refreshTokenTtlSeconds },
924
+ {
925
+ sub: user.id.toString(),
926
+ sessionId: session.id,
927
+ sv: session.sessionVersion,
928
+ },
929
+ {
930
+ jti: session.currentRefreshJti,
931
+ ttlSeconds: this.config.refreshTokenTtlSeconds,
932
+ },
538
933
  );
539
934
 
540
- return { accessToken: access.token, refreshToken: refresh.token, sessionId: session.id };
935
+ return {
936
+ accessToken: access.token,
937
+ refreshToken: refresh.token,
938
+ sessionId: session.id,
939
+ };
541
940
  }
542
941
  }