@rebasepro/server 0.21.1-canary.g8c5a265 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/api/errors.d.ts +41 -0
  2. package/dist/auth/auth-hooks.d.ts +4 -0
  3. package/dist/auth/reset-password-admin.d.ts +4 -0
  4. package/dist/auth/token-revocation.d.ts +39 -5
  5. package/dist/{auth-BvBllZjy.js → auth-BS4WBA10.js} +98 -57
  6. package/dist/auth-BS4WBA10.js.map +1 -0
  7. package/dist/{backup-De-Xm4tg.js → backup-DzI9jLwc.js} +2 -2
  8. package/dist/{backup-De-Xm4tg.js.map → backup-DzI9jLwc.js.map} +1 -1
  9. package/dist/{cron-routes-3FVef5QO.js → cron-routes-B_wlLybo.js} +3 -3
  10. package/dist/{cron-routes-3FVef5QO.js.map → cron-routes-B_wlLybo.js.map} +1 -1
  11. package/dist/ddl-bootstrap-CfNvxMuK.js.map +1 -1
  12. package/dist/{errors-DMImyqyR.js → errors-DWsX4yTd.js} +55 -18
  13. package/dist/errors-DWsX4yTd.js.map +1 -0
  14. package/dist/{function-routes-C4nB2h0z.js → function-routes-Chet4-lB.js} +2 -2
  15. package/dist/{function-routes-C4nB2h0z.js.map → function-routes-Chet4-lB.js.map} +1 -1
  16. package/dist/functions/index.js.map +1 -1
  17. package/dist/index.es.js +39 -32
  18. package/dist/index.es.js.map +1 -1
  19. package/dist/{logs-routes-DnJINsMu.js → logs-routes-3EEzPjhl.js} +2 -2
  20. package/dist/{logs-routes-DnJINsMu.js.map → logs-routes-3EEzPjhl.js.map} +1 -1
  21. package/dist/{query-parser-DUl8d557.js → query-parser-C-rl30ce.js} +2 -2
  22. package/dist/{query-parser-DUl8d557.js.map → query-parser-C-rl30ce.js.map} +1 -1
  23. package/dist/{request-timeout-BR-OBwES.js → request-timeout-C_4C2BeR.js} +2 -2
  24. package/dist/{request-timeout-BR-OBwES.js.map → request-timeout-C_4C2BeR.js.map} +1 -1
  25. package/dist/{schema-editor-routes-BKOmdf4M.js → schema-editor-routes-DdLihzp0.js} +2 -2
  26. package/dist/{schema-editor-routes-BKOmdf4M.js.map → schema-editor-routes-DdLihzp0.js.map} +1 -1
  27. package/dist/src-Br6ARbs6.js.map +1 -1
  28. package/package.json +5 -5
  29. package/dist/auth-BvBllZjy.js.map +0 -1
  30. package/dist/errors-DMImyqyR.js.map +0 -1
@@ -109,6 +109,47 @@ export interface RebaseApiError extends Error {
109
109
  code?: string;
110
110
  details?: unknown;
111
111
  }
112
+ /**
113
+ * The answer an error chose for itself, read the same way at every door.
114
+ *
115
+ * @see declaredErrorAnswer
116
+ */
117
+ export interface DeclaredErrorAnswer {
118
+ /** The HTTP status the error carries. A socket frame has no slot for it. */
119
+ status: number;
120
+ code: string;
121
+ message: string;
122
+ details?: unknown;
123
+ /** See {@link ApiError.expected}: log it at debug, not warn. */
124
+ expected: boolean;
125
+ }
126
+ /**
127
+ * The status, code and message an error carries as its own answer — or
128
+ * `undefined` for an error that carries none, which is a server fault and gets
129
+ * masked.
130
+ *
131
+ * Two classes carry one. The server's `ApiError`, and `RebaseApiError` (or its
132
+ * `RebaseClientError` subclass) from `@rebasepro/types` once it has a status.
133
+ * The second is the browser-safe class: a `config/collections/*.ts` file is
134
+ * bundled into the admin SPA and cannot import this package, so it is what a
135
+ * collection callback throws, and what a callback refusal becomes —
136
+ * `callbackRefusal` returns one, and `toCallbackError` wraps anything thrown
137
+ * that does not already carry a status.
138
+ *
139
+ * One function because several doors turn an error into an answer: the REST
140
+ * error handler, the two WebSocket servers, and the Postgres realtime
141
+ * subscriptions. Each used to list the classes it recognised by hand. The
142
+ * sockets listed only `ApiError`, so a `beforeDelete` veto that REST
143
+ * answered as 400 `CALLBACK_REJECTED` with the author's message reached the
144
+ * admin panel — which writes through the socket — as `INTERNAL_ERROR`, and in
145
+ * production as "An unexpected error occurred".
146
+ *
147
+ * Matched by name as well as `instanceof`: a monorepo can resolve two copies of
148
+ * a package, and `instanceof` is false across them. Name matching is also why
149
+ * this file needs no runtime import of `@rebasepro/types`, which it may not
150
+ * have — it is in the graph of `@rebasepro/server/functions`.
151
+ */
152
+ export declare function declaredErrorAnswer(error: unknown): DeclaredErrorAnswer | undefined;
112
153
  /**
113
154
  * Hono error-handling middleware (`app.onError`).
114
155
  * Converts any error into the canonical `{ error: { message, code } }` shape.
@@ -222,6 +222,10 @@ export interface AuthHooks {
222
222
  /**
223
223
  * Optional hook to customize or override the default password reset flow via the admin panel.
224
224
  * When provided, this replaces the built-in password reset token generation, hashing, and email logic.
225
+ *
226
+ * A `temporaryPassword` it returns becomes the account's password: the route hashes and writes
227
+ * it (the same contract as a collection's `auth.onResetPassword`), so the hook need not write it
228
+ * through `authRepo`. The account's existing sessions end whatever the hook returns.
225
229
  */
226
230
  onAdminResetPassword?(uid: string, ctx: {
227
231
  authRepo: AuthRepository;
@@ -5,6 +5,10 @@
5
5
  * 1. Collection-level hook (`auth.onResetPassword` on the collection)
6
6
  * 2. Backend-level hook (`AuthHooks.onAdminResetPassword`)
7
7
  * 3. Built-in default (send reset email, or generate temp password)
8
+ *
9
+ * Whichever of them runs, the account's existing sessions end, and a
10
+ * `temporaryPassword` in its result becomes the account's password before the
11
+ * response shows it to the admin.
8
12
  */
9
13
  import { Hono } from "hono";
10
14
  import type { AuthRepository } from "./interfaces.js";
@@ -3,11 +3,12 @@ import type { AccessTokenPayload } from "./jwt.js";
3
3
  /**
4
4
  * Has this access token been revoked?
5
5
  *
6
- * `logout`, `change-password`, `reset-password` and `DELETE /auth/sessions` all
7
- * stamp a `tokensValidAfter` watermark on the user. Every one of them also
8
- * deletes refresh-token rows, which is what made the gap easy to miss: the
9
- * session really is gone, and the *refresh* path really does check the
10
- * watermark so signing out looked like it worked.
6
+ * Everything that ends every session a user holds — a password change or reset
7
+ * of any kind, and `DELETE /auth/sessions` goes through
8
+ * {@link revokeAllSessions}, which stamps a `tokensValidAfter` watermark on the
9
+ * user. It also deletes refresh-token rows, which is what made the gap easy to
10
+ * miss: the session really is gone, and the *refresh* path really does check
11
+ * the watermark — so signing out looked like it worked.
11
12
  *
12
13
  * The access token was untouched. It is a bearer credential that nothing
13
14
  * consulted a database about, so it stayed valid for its full lifetime after
@@ -36,3 +37,36 @@ import type { AccessTokenPayload } from "./jwt.js";
36
37
  * failure is logged at warn so it is visible rather than silent.
37
38
  */
38
39
  export declare function isAccessTokenRevoked(authRepo: Pick<AuthRepository, "getTokensValidAfter">, payload: Pick<AccessTokenPayload, "uid" | "iat">): Promise<boolean>;
40
+ /**
41
+ * End every session this user holds, on every device.
42
+ *
43
+ * Two writes, because each covers what the other cannot. Deleting the refresh
44
+ * rows ends the sessions that exist at this instant, and on a repository
45
+ * without the watermark it is the only revocation there is. The watermark
46
+ * voids what the delete cannot see: a refresh already in flight that inserts
47
+ * its rotated token a moment after the delete ran, and every access token
48
+ * already handed out, which {@link isAccessTokenRevoked} refuses from here on.
49
+ *
50
+ * The watermark write does not fail the caller. By the time it runs the
51
+ * credential has usually already changed and the rows are gone, so a 500 would
52
+ * tell someone their password did not change when it did. It is logged, because
53
+ * a failure here leaves access tokens working until they expire.
54
+ */
55
+ export declare function revokeAllSessions(authRepo: Pick<AuthRepository, "deleteAllRefreshTokensForUser" | "setTokensValidAfter">, uid: string): Promise<void>;
56
+ /**
57
+ * Replace a user's password and end every session they hold.
58
+ *
59
+ * The one way a password hash is written over an existing account. A new
60
+ * password is what someone sets when they believe the old one — or a session
61
+ * signed in with it — is in someone else's hands, so a password change that
62
+ * leaves those sessions alive does not do the thing it was done for.
63
+ *
64
+ * It used to be a pair of lines repeated after each `updatePassword`, and it
65
+ * reached the two self-service routes and none of the admin ones: an
66
+ * administrator resetting a phished account left the attacker's refresh token
67
+ * minting access tokens for the rest of its lifetime.
68
+ * `test/password-change-revokes-sessions.test.ts` holds every route that sets a
69
+ * password to this, and fails if anything but this function calls
70
+ * `updatePassword`.
71
+ */
72
+ export declare function replaceUserPassword(authRepo: Pick<AuthRepository, "updatePassword" | "deleteAllRefreshTokensForUser" | "setTokensValidAfter">, uid: string, passwordHash: string): Promise<void>;
@@ -8,8 +8,8 @@ import "./src-Br6ARbs6.js";
8
8
  import { n as createDdlBootstrapper, o as isSQLAdmin } from "./ddl-bootstrap-CfNvxMuK.js";
9
9
  import { t as firstSqlRow } from "./sql-rows-C6GEc2oE.js";
10
10
  import { r as logger } from "./logger-DO2PZc4i.js";
11
- import { n as errorHandler, t as ApiError } from "./errors-DMImyqyR.js";
12
- import { a as resolveListLimitParam } from "./query-parser-DUl8d557.js";
11
+ import { r as errorHandler, t as ApiError } from "./errors-DWsX4yTd.js";
12
+ import { a as resolveListLimitParam } from "./query-parser-C-rl30ce.js";
13
13
  import { C as require_jsonwebtoken, E as randomInt$1, O as sha256Hex, _ as verifyAccessToken, b as verifyMfaPendingToken, c as getAccessTokenExpiry, d as getRefreshTokenTtlMs, f as hasAsymmetricSigningKey, i as generateDownloadToken, l as getJwks, m as isJwtConfigured, n as configureJwt, o as generateMfaPendingToken, p as hashRefreshToken, r as generateAccessToken, s as generateRefreshToken, t as MAX_COOKIE_AGE_MS, u as getRefreshTokenExpiry, v as verifyDownloadToken, w as constantTimeEqual } from "./jwt-C4OW-DNq.js";
14
14
  import { n as hasAdministrativeRole, r as isAdministrativeRole, t as ADMINISTRATIVE_ROLES } from "./admin-roles-vYdp_Pil.js";
15
15
  import { c as tryCanonicalStorageKey, i as canonicalStorageId } from "./keys-Qfc4XieN.js";
@@ -1034,11 +1034,12 @@ async function completeUserCreation(prepared, finalize) {
1034
1034
  /**
1035
1035
  * Has this access token been revoked?
1036
1036
  *
1037
- * `logout`, `change-password`, `reset-password` and `DELETE /auth/sessions` all
1038
- * stamp a `tokensValidAfter` watermark on the user. Every one of them also
1039
- * deletes refresh-token rows, which is what made the gap easy to miss: the
1040
- * session really is gone, and the *refresh* path really does check the
1041
- * watermark so signing out looked like it worked.
1037
+ * Everything that ends every session a user holds — a password change or reset
1038
+ * of any kind, and `DELETE /auth/sessions` goes through
1039
+ * {@link revokeAllSessions}, which stamps a `tokensValidAfter` watermark on the
1040
+ * user. It also deletes refresh-token rows, which is what made the gap easy to
1041
+ * miss: the session really is gone, and the *refresh* path really does check
1042
+ * the watermark — so signing out looked like it worked.
1042
1043
  *
1043
1044
  * The access token was untouched. It is a bearer credential that nothing
1044
1045
  * consulted a database about, so it stayed valid for its full lifetime after
@@ -1082,6 +1083,52 @@ async function isAccessTokenRevoked(authRepo, payload) {
1082
1083
  if (!validAfter) return false;
1083
1084
  return payload.iat < Math.floor(validAfter.getTime() / 1e3);
1084
1085
  }
1086
+ /**
1087
+ * End every session this user holds, on every device.
1088
+ *
1089
+ * Two writes, because each covers what the other cannot. Deleting the refresh
1090
+ * rows ends the sessions that exist at this instant, and on a repository
1091
+ * without the watermark it is the only revocation there is. The watermark
1092
+ * voids what the delete cannot see: a refresh already in flight that inserts
1093
+ * its rotated token a moment after the delete ran, and every access token
1094
+ * already handed out, which {@link isAccessTokenRevoked} refuses from here on.
1095
+ *
1096
+ * The watermark write does not fail the caller. By the time it runs the
1097
+ * credential has usually already changed and the rows are gone, so a 500 would
1098
+ * tell someone their password did not change when it did. It is logged, because
1099
+ * a failure here leaves access tokens working until they expire.
1100
+ */
1101
+ async function revokeAllSessions(authRepo, uid) {
1102
+ await authRepo.deleteAllRefreshTokensForUser(uid);
1103
+ try {
1104
+ await authRepo.setTokensValidAfter?.(uid, /* @__PURE__ */ new Date());
1105
+ } catch (error) {
1106
+ logger.warn("[Auth] Could not write the token revocation watermark; access tokens issued before now stay valid until they expire", {
1107
+ uid,
1108
+ error
1109
+ });
1110
+ }
1111
+ }
1112
+ /**
1113
+ * Replace a user's password and end every session they hold.
1114
+ *
1115
+ * The one way a password hash is written over an existing account. A new
1116
+ * password is what someone sets when they believe the old one — or a session
1117
+ * signed in with it — is in someone else's hands, so a password change that
1118
+ * leaves those sessions alive does not do the thing it was done for.
1119
+ *
1120
+ * It used to be a pair of lines repeated after each `updatePassword`, and it
1121
+ * reached the two self-service routes and none of the admin ones: an
1122
+ * administrator resetting a phished account left the attacker's refresh token
1123
+ * minting access tokens for the rest of its lifetime.
1124
+ * `test/password-change-revokes-sessions.test.ts` holds every route that sets a
1125
+ * password to this, and fails if anything but this function calls
1126
+ * `updatePassword`.
1127
+ */
1128
+ async function replaceUserPassword(authRepo, uid, passwordHash) {
1129
+ await authRepo.updatePassword(uid, passwordHash);
1130
+ await revokeAllSessions(authRepo, uid);
1131
+ }
1085
1132
  //#endregion
1086
1133
  //#region src/auth/rls-scope.ts
1087
1134
  var import_jsonwebtoken = /* @__PURE__ */ __toESM(require_jsonwebtoken(), 1);
@@ -3633,8 +3680,7 @@ function mountSessionRoutes(opts) {
3633
3680
  router.delete("/sessions", requireLiveSession, async (c) => {
3634
3681
  const userCtx = c.get("user");
3635
3682
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
3636
- await authRepo.deleteAllRefreshTokensForUser(userCtx.uid);
3637
- await authRepo.setTokensValidAfter?.(userCtx.uid, /* @__PURE__ */ new Date()).catch(() => void 0);
3683
+ await revokeAllSessions(authRepo, userCtx.uid);
3638
3684
  return c.json({
3639
3685
  success: true,
3640
3686
  message: "All sessions revoked successfully"
@@ -4548,10 +4594,8 @@ function createAuthRoutes(config) {
4548
4594
  const storedToken = await authRepo.findValidPasswordResetToken(tokenHash);
4549
4595
  if (!storedToken) throw ApiError.badRequest("Invalid or expired reset token", "INVALID_TOKEN");
4550
4596
  const passwordHash = await ops.hashPassword(password);
4551
- await authRepo.updatePassword(storedToken.uid, passwordHash);
4597
+ await replaceUserPassword(authRepo, storedToken.uid, passwordHash);
4552
4598
  await authRepo.markPasswordResetTokenUsed(tokenHash);
4553
- await authRepo.deleteAllRefreshTokensForUser(storedToken.uid);
4554
- await authRepo.setTokensValidAfter?.(storedToken.uid, /* @__PURE__ */ new Date()).catch(() => void 0);
4555
4599
  if (ops.onPasswordReset) ops.onPasswordReset(storedToken.uid).catch((err) => {
4556
4600
  logger.error("[AuthHooks] onPasswordReset error", { error: err instanceof Error ? err.message : err });
4557
4601
  });
@@ -4574,9 +4618,7 @@ function createAuthRoutes(config) {
4574
4618
  const passwordValidation = ops.validatePasswordStrength(newPassword);
4575
4619
  if (!passwordValidation.valid) throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
4576
4620
  const passwordHash = await ops.hashPassword(newPassword);
4577
- await authRepo.updatePassword(user.id, passwordHash);
4578
- await authRepo.deleteAllRefreshTokensForUser(user.id);
4579
- await authRepo.setTokensValidAfter?.(user.id, /* @__PURE__ */ new Date()).catch(() => void 0);
4621
+ await replaceUserPassword(authRepo, user.id, passwordHash);
4580
4622
  return c.json({
4581
4623
  success: true,
4582
4624
  message: "Password has been changed successfully"
@@ -4809,6 +4851,10 @@ function buildBuiltinAuthCapabilities(inputs) {
4809
4851
  * 1. Collection-level hook (`auth.onResetPassword` on the collection)
4810
4852
  * 2. Backend-level hook (`AuthHooks.onAdminResetPassword`)
4811
4853
  * 3. Built-in default (send reset email, or generate temp password)
4854
+ *
4855
+ * Whichever of them runs, the account's existing sessions end, and a
4856
+ * `temporaryPassword` in its result becomes the account's password before the
4857
+ * response shows it to the admin.
4812
4858
  */
4813
4859
  /**
4814
4860
  * Create a standalone admin route for resetting user passwords.
@@ -4834,14 +4880,10 @@ function createResetPasswordRoute(config) {
4834
4880
  let temporaryPassword;
4835
4881
  let emailDeliveryFailed = false;
4836
4882
  const body = await c.req.json().catch(() => ({}));
4837
- if (body.password) {
4838
- const password = body.password;
4839
- const validation = ops.validatePasswordStrength(password);
4883
+ const chosenPassword = body.password ? body.password : void 0;
4884
+ if (chosenPassword) {
4885
+ const validation = ops.validatePasswordStrength(chosenPassword);
4840
4886
  if (!validation.valid) throw ApiError.badRequest(`Password too weak: ${validation.errors.join(", ")}`);
4841
- const passwordHash = await ops.hashPassword(password);
4842
- await authRepo.updatePassword(existing.id, passwordHash);
4843
- temporaryPassword = void 0;
4844
- invitationSent = false;
4845
4887
  } else if (collectionAuthConfig?.onResetPassword) {
4846
4888
  const isEmailConfigured = !!(emailService && emailService.isConfigured());
4847
4889
  const hookResult = await collectionAuthConfig.onResetPassword(existing.id, {
@@ -4861,42 +4903,38 @@ function createResetPasswordRoute(config) {
4861
4903
  });
4862
4904
  temporaryPassword = hookResult.temporaryPassword;
4863
4905
  invitationSent = hookResult.invitationSent ?? false;
4864
- } else if (!!(emailService && emailService.isConfigured())) try {
4906
+ } else if (!!(emailService && emailService.isConfigured())) {
4865
4907
  const token = generateSecureToken();
4866
4908
  const tokenHash = hashToken$1(token);
4867
4909
  const expiresAt = new Date(Date.now() + 3600 * 1e3);
4868
4910
  await authRepo.createPasswordResetToken(existing.id, tokenHash, expiresAt);
4869
- const setPasswordUrl = `${emailConfig?.resetPasswordUrl || ""}/reset-password?token=${token}`;
4870
- const { appName, logoUrl } = resolveEmailBranding(emailConfig);
4871
- const templateFn = emailConfig?.templates?.passwordReset;
4872
- const emailContent = templateFn ? templateFn(setPasswordUrl, {
4873
- email: existing.email,
4874
- displayName: existing.displayName
4875
- }) : getPasswordResetTemplate(setPasswordUrl, {
4876
- email: existing.email,
4877
- displayName: existing.displayName
4878
- }, appName, logoUrl);
4879
- await emailService.send({
4880
- to: existing.email,
4881
- subject: emailContent.subject,
4882
- html: emailContent.html,
4883
- text: emailContent.text
4884
- });
4885
- invitationSent = true;
4886
- } catch (emailError) {
4887
- logger.error("Failed to send reset email", { error: emailError instanceof Error ? emailError.message : emailError });
4888
- const clearPassword = generateSecurePassword();
4889
- const passwordHash = await ops.hashPassword(clearPassword);
4890
- await authRepo.updatePassword(existing.id, passwordHash);
4891
- temporaryPassword = clearPassword;
4892
- emailDeliveryFailed = true;
4893
- }
4894
- else {
4895
- const clearPassword = generateSecurePassword();
4896
- const passwordHash = await ops.hashPassword(clearPassword);
4897
- await authRepo.updatePassword(existing.id, passwordHash);
4898
- temporaryPassword = clearPassword;
4899
- }
4911
+ try {
4912
+ const setPasswordUrl = `${emailConfig?.resetPasswordUrl || ""}/reset-password?token=${token}`;
4913
+ const { appName, logoUrl } = resolveEmailBranding(emailConfig);
4914
+ const templateFn = emailConfig?.templates?.passwordReset;
4915
+ const emailContent = templateFn ? templateFn(setPasswordUrl, {
4916
+ email: existing.email,
4917
+ displayName: existing.displayName
4918
+ }) : getPasswordResetTemplate(setPasswordUrl, {
4919
+ email: existing.email,
4920
+ displayName: existing.displayName
4921
+ }, appName, logoUrl);
4922
+ await emailService.send({
4923
+ to: existing.email,
4924
+ subject: emailContent.subject,
4925
+ html: emailContent.html,
4926
+ text: emailContent.text
4927
+ });
4928
+ invitationSent = true;
4929
+ } catch (emailError) {
4930
+ logger.error("Failed to send reset email", { error: emailError instanceof Error ? emailError.message : emailError });
4931
+ temporaryPassword = generateSecurePassword();
4932
+ emailDeliveryFailed = true;
4933
+ }
4934
+ } else temporaryPassword = generateSecurePassword();
4935
+ const newPassword = chosenPassword ?? temporaryPassword;
4936
+ if (newPassword) await replaceUserPassword(authRepo, existing.id, await ops.hashPassword(newPassword));
4937
+ else await revokeAllSessions(authRepo, existing.id);
4900
4938
  const userRoles = await authRepo.getUserRoleIds(existing.id);
4901
4939
  return c.json({
4902
4940
  user: {
@@ -5124,12 +5162,14 @@ function createAdminUsersRoute(config) {
5124
5162
  const updates = {};
5125
5163
  if (email !== void 0) updates.email = normalizeEmail(email);
5126
5164
  if (displayName !== void 0) updates.displayName = displayName;
5165
+ let passwordHash;
5127
5166
  if (password) {
5128
5167
  const validation = ops.validatePasswordStrength(password);
5129
5168
  if (!validation.valid) throw ApiError.badRequest(`Password too weak: ${validation.errors.join(". ")}`);
5130
- updates.passwordHash = await ops.hashPassword(password);
5169
+ passwordHash = await ops.hashPassword(password);
5131
5170
  }
5132
5171
  if (Object.keys(updates).length > 0) await authRepo.updateUser(uid, updates);
5172
+ if (passwordHash) await replaceUserPassword(authRepo, uid, passwordHash);
5133
5173
  if (roles !== void 0 && Array.isArray(roles)) {
5134
5174
  const wasAdmin = (await authRepo.getUserRoleIds(uid)).includes("admin");
5135
5175
  const willBeAdmin = roles.includes("admin");
@@ -5396,8 +5436,9 @@ function createUserManagementFromRepo(repo, resolvedOps) {
5396
5436
  if (data.displayName !== void 0) updateData.displayName = data.displayName;
5397
5437
  if (data.photoUrl !== void 0) updateData.photoUrl = data.photoUrl;
5398
5438
  if (data.metadata !== void 0) updateData.metadata = data.metadata;
5399
- if (data.password) updateData.passwordHash = await resolvedOps.hashPassword(data.password);
5439
+ const passwordHash = data.password ? await resolvedOps.hashPassword(data.password) : void 0;
5400
5440
  const user = await repo.updateUser(id, updateData);
5441
+ if (user && passwordHash) await replaceUserPassword(repo, id, passwordHash);
5401
5442
  return user ? toAuthUserData(user) : null;
5402
5443
  },
5403
5444
  async deleteUser(id) {
@@ -7137,4 +7178,4 @@ var auth_exports = /* @__PURE__ */ __exportAll({
7137
7178
  //#endregion
7138
7179
  export { requireAdmin as $, defaultAuthLimiter as A, resolveEmailLinkBase as B, providerVerifiedEmail as C, raw as Ct, DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT as D, isPublicStoragePath as Dt, isBootstrapWindowOpen as E, PUBLIC_STORAGE_PREFIX as Et, extractLinks as F, createAdapterAuthMiddleware as G, hashPassword as H, registerDevEmailSink as I, extractUserFromToken as J, createAuthMiddleware as K, SMTPEmailService as L, activeDevEmailSink as M, clearActiveDevEmailSink as N, createDataRateLimiter as O, createDevEmailSink as P, queryTokenAuth as Q, createEmailService as R, pkceTokenParams as S, html as St, createBuiltinAuthAdapter as T, isOperationAllowed as Tt, validatePasswordStrength as U, resolveAuthHooks as V, verifyPassword as W, optionalAuth as X, fileTokenAuth as Y, publicObjectAuth as Z, verifyOidcIdToken as _, getUserInvitationTemplate as _t, resolveRateLimitStoreKind as a, validateApiKey as at, createGoogleProvider as b, RawHtml as bt, createSlackProvider as c, SERVICE_IDENTITY as ct, createDiscordProvider as d, completeUserCreation as dt, requireAuth as et, createTwitterProvider as f, generateSecurePassword as ft, tryVerifyOidcIdToken as g, getPasswordResetTemplate as gt, createMicrosoftProvider as h, getMagicLinkTemplate as ht, createApiKeyStore as i, isApiKeyToken as it, MemoryRateLimitStore as j, createRateLimiter as k, createBitbucketProvider as l, scopeDataDriver as lt, createAppleProvider as m, getEmailVerificationTemplate as mt, createCustomAuthAdapter as n, createFunctionApiKeyGuard as nt, createSqlRateLimitStore as o, extractBearerToken as ot, createFacebookProvider as p, getEmailOtpTemplate as pt, createRequireAuth as q, createApiKeyRoutes as r, createStorageApiKeyGuard as rt, createSpotifyProvider as s, safeCompare as st, auth_exports as t, createApiKeyPreAuth as tt, createGitLabProvider as u, supportsRlsScoping as ut, createGitHubProvider as v, getWelcomeEmailTemplate as vt, createJwksRoutes as w, httpMethodToOperation as wt, oauthCodeFlowSchema as x, escapeHtml as xt, createLinkedinProvider as y, resolveEmailBranding as yt, assertEmailLinkBases as z };
7139
7180
 
7140
- //# sourceMappingURL=auth-BvBllZjy.js.map
7181
+ //# sourceMappingURL=auth-BS4WBA10.js.map