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