najm-auth 4.2.4 → 5.0.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.
package/dist/index.js CHANGED
@@ -498,7 +498,7 @@ import { Get, Post, ResMsg } from "najm-core";
498
498
  import { Body, User as User2, Headers, Ctx } from "najm-core";
499
499
 
500
500
  // src/auth/AuthService.ts
501
- import { Injectable as Injectable12, Inject as Inject12 } from "najm-core";
501
+ import { Injectable as Injectable12, Inject as Inject13 } from "najm-core";
502
502
  import { Err as Err12, Log } from "najm-core";
503
503
  import { Transaction as Transaction4 } from "najm-database";
504
504
  import { I18n as I18n8, I18nService as I18nService2 } from "najm-i18n";
@@ -512,7 +512,7 @@ import { I18nService } from "najm-i18n";
512
512
  import { I18n as I18n4 } from "najm-i18n";
513
513
 
514
514
  // src/users/UserRepository.ts
515
- import { eq as eq2, ne, sql as sql3 } from "drizzle-orm";
515
+ import { and, eq as eq2, isNull, ne, sql as sql3 } from "drizzle-orm";
516
516
  import { Repository, Inject as Inject3 } from "najm-core";
517
517
  import { DB } from "najm-database";
518
518
 
@@ -699,6 +699,11 @@ var UserRepository = class UserRepository2 {
699
699
  const [updatedUser] = await this.db.update(this.users).set(data).where(eq2(this.users.id, id)).returning();
700
700
  return updatedUser;
701
701
  }
702
+ /** The credential that authorized a delayed write must still own the account. */
703
+ async updateWithCredential(id, data, expected) {
704
+ const [updated] = await this.db.update(this.users).set(data).where(and(eq2(this.users.id, id), eq2(this.users.password, expected.password), eq2(this.users.email, expected.email), expected.status == null ? isNull(this.users.status) : eq2(this.users.status, expected.status), expected.emailVerified == null ? isNull(this.users.emailVerified) : eq2(this.users.emailVerified, expected.emailVerified))).returning();
705
+ return updated;
706
+ }
702
707
  async updateLastLogin(id) {
703
708
  const [updatedUser] = await this.db.update(this.users).set({ lastLogin: (/* @__PURE__ */ new Date()).toISOString() }).where(eq2(this.users.id, id)).returning();
704
709
  return updatedUser;
@@ -758,6 +763,24 @@ UserRepository = __decorate3([
758
763
  import { Injectable as Injectable2 } from "najm-core";
759
764
  import { Err } from "najm-core";
760
765
  import { I18n } from "najm-i18n";
766
+
767
+ // src/shared/uniqueViolation.ts
768
+ function isUniqueViolation(error) {
769
+ const visited = /* @__PURE__ */ new Set();
770
+ while (error && typeof error === "object" && !visited.has(error)) {
771
+ visited.add(error);
772
+ const candidate = error;
773
+ if (candidate.code === "23505" || candidate.code === "SQLITE_CONSTRAINT_UNIQUE" || candidate.code === "SQLITE_CONSTRAINT_PRIMARYKEY")
774
+ return true;
775
+ if (typeof candidate.message === "string" && /unique constraint failed/i.test(candidate.message))
776
+ return true;
777
+ error = candidate.cause;
778
+ }
779
+ return false;
780
+ }
781
+ __name(isUniqueViolation, "isUniqueViolation");
782
+
783
+ // src/users/UserValidator.ts
761
784
  var __decorate4 = function(decorators, target, key, desc) {
762
785
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
763
786
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -786,7 +809,7 @@ var UserValidator = class UserValidator2 {
786
809
  async checkEmailUnique(email2, excludeId) {
787
810
  if (!email2)
788
811
  return;
789
- const existingUser = await this.userRepository.getByEmail(email2);
812
+ const existingUser = await this.userRepository.getByEmailInsensitive(email2);
790
813
  if (existingUser && existingUser.id !== excludeId) {
791
814
  Err(this.t("errors.emailExists"), 409);
792
815
  }
@@ -802,6 +825,20 @@ var UserValidator = class UserValidator2 {
802
825
  Err(this.t("errors.phoneExists"), 409);
803
826
  }
804
827
  }
828
+ /** The database remains authoritative when two validation reads both pass. */
829
+ async writeUnique(write) {
830
+ try {
831
+ return await write();
832
+ } catch (error) {
833
+ if (isUniqueViolation(error))
834
+ Err("User identity already exists", 409);
835
+ throw error;
836
+ }
837
+ }
838
+ checkCredentialUnchanged(updated) {
839
+ if (!updated)
840
+ Err("Account credentials changed. Please sign in again or request a new link.", 409);
841
+ }
805
842
  /**
806
843
  * Check if user exists by ID
807
844
  */
@@ -919,7 +956,7 @@ UserValidator = __decorate4([
919
956
  ], UserValidator);
920
957
 
921
958
  // src/roles/RoleService.ts
922
- import { Injectable as Injectable4, Err as Err3 } from "najm-core";
959
+ import { Injectable as Injectable5, Err as Err3 } from "najm-core";
923
960
  import { I18n as I18n3 } from "najm-i18n";
924
961
 
925
962
  // src/roles/RoleRepository.ts
@@ -1069,7 +1106,16 @@ RoleValidator = __decorate6([
1069
1106
  __metadata6("design:paramtypes", [typeof (_a3 = typeof RoleRepository !== "undefined" && RoleRepository) === "function" ? _a3 : Object])
1070
1107
  ], RoleValidator);
1071
1108
 
1072
- // src/roles/RoleService.ts
1109
+ // src/tokens/SessionInvalidationService.ts
1110
+ import { Inject as Inject6, Injectable as Injectable4 } from "najm-core";
1111
+ import { CacheService } from "najm-cache";
1112
+ import { TransactionService } from "najm-database";
1113
+ import timestring2 from "timestring";
1114
+
1115
+ // src/tokens/TokenRepository.ts
1116
+ import { and as and2, eq as eq4, isNull as isNull2, lt } from "drizzle-orm";
1117
+ import { Repository as Repository3, Inject as Inject5 } from "najm-core";
1118
+ import { DB as DB3 } from "najm-database";
1073
1119
  var __decorate7 = function(decorators, target, key, desc) {
1074
1120
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1075
1121
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1079,487 +1125,177 @@ var __decorate7 = function(decorators, target, key, desc) {
1079
1125
  var __metadata7 = function(k, v) {
1080
1126
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1081
1127
  };
1082
- var _a4;
1083
- var _b2;
1084
- var RoleService = class RoleService2 {
1128
+ var TokenRepository = class TokenRepository2 {
1085
1129
  static {
1086
- __name(this, "RoleService");
1130
+ __name(this, "TokenRepository");
1087
1131
  }
1088
- roleRepository;
1089
- roleValidator;
1090
- t;
1091
- constructor(roleRepository, roleValidator) {
1092
- this.roleRepository = roleRepository;
1093
- this.roleValidator = roleValidator;
1132
+ db;
1133
+ schema;
1134
+ get tokens() {
1135
+ return this.schema.tokens;
1094
1136
  }
1095
- async getAll() {
1096
- return await this.roleRepository.getAll();
1137
+ get users() {
1138
+ return this.schema.users;
1097
1139
  }
1098
- async getById(id) {
1099
- await this.roleValidator.checkRoleExists(id);
1100
- return await this.roleRepository.getById(id);
1140
+ async getCredentialState(userId) {
1141
+ const [user] = await this.db.select({
1142
+ id: this.users.id,
1143
+ password: this.users.password,
1144
+ email: this.users.email,
1145
+ status: this.users.status,
1146
+ emailVerified: this.users.emailVerified
1147
+ }).from(this.users).where(eq4(this.users.id, userId)).limit(1);
1148
+ return user;
1101
1149
  }
1102
- async getByName(name) {
1103
- return await this.roleRepository.getByName(name);
1150
+ /** Shared query helper, scoped to the current database/transaction identity. */
1151
+ queryHelper;
1152
+ get q() {
1153
+ const db = this.db;
1154
+ if (this.queryHelper?.db !== db) {
1155
+ this.queryHelper = { db, queries: new AuthQueries(db, this.schema) };
1156
+ }
1157
+ return this.queryHelper.queries;
1104
1158
  }
1105
- async create(data) {
1106
- await this.roleValidator.checkNameUnique(data.name);
1107
- return await this.onDuplicateName(() => this.roleRepository.create(data));
1159
+ /**
1160
+ * Upsert the refresh-token row for a session, keyed on `tokenFamily` (the
1161
+ * per-login session identifier, unique). A brand-new login inserts a fresh
1162
+ * family row; a refresh rotation updates only that family's row, leaving the
1163
+ * user's other sessions untouched.
1164
+ */
1165
+ async storeRefreshToken(tokenData) {
1166
+ return await this.db.insert(this.tokens).values(tokenData).onConflictDoUpdate({
1167
+ target: this.tokens.tokenFamily,
1168
+ set: {
1169
+ token: tokenData.token,
1170
+ expiresAt: tokenData.expiresAt,
1171
+ previousHash: tokenData.previousHash ?? null,
1172
+ previousValidUntil: tokenData.previousValidUntil ?? null,
1173
+ previousUsedAt: tokenData.previousUsedAt ?? null
1174
+ },
1175
+ // A revoked family is a durable tombstone. It must never be revived by
1176
+ // an issuance racing with logout, or after the cache markers are lost.
1177
+ setWhere: and2(eq4(this.tokens.status, "active"), eq4(this.tokens.userId, tokenData.userId))
1178
+ }).returning();
1108
1179
  }
1109
- async update(id, data) {
1110
- const role = await this.roleValidator.checkRoleExists(id);
1111
- if (role.name === ROLES.ADMIN && data.name && data.name !== ROLES.ADMIN) {
1112
- Err3(this.t("errors.cannotRenameSystem"), 403);
1113
- }
1114
- await this.roleValidator.checkNameUnique(data.name, id);
1115
- return await this.onDuplicateName(() => this.roleRepository.update(id, data));
1180
+ /**
1181
+ * Rotate an existing refresh-token family with compare-and-swap semantics.
1182
+ * This can never update a family durably revoked by a concurrent logout.
1183
+ */
1184
+ async rotateRefreshToken(tokenData, expectedCurrentHash) {
1185
+ return await this.db.update(this.tokens).set({
1186
+ token: tokenData.token,
1187
+ expiresAt: tokenData.expiresAt,
1188
+ previousHash: tokenData.previousHash,
1189
+ previousValidUntil: tokenData.previousValidUntil,
1190
+ previousUsedAt: tokenData.previousUsedAt ?? null
1191
+ }).where(and2(eq4(this.tokens.tokenFamily, tokenData.tokenFamily), eq4(this.tokens.userId, tokenData.userId), eq4(this.tokens.token, expectedCurrentHash), eq4(this.tokens.status, "active"))).returning();
1116
1192
  }
1117
- async delete(id) {
1118
- const role = await this.roleValidator.checkRoleExists(id);
1119
- if (role.name === ROLES.ADMIN) {
1120
- Err3(this.t("errors.cannotDeleteSystem"), 403);
1121
- }
1122
- if (await this.roleRepository.hasUsers(id)) {
1123
- Err3(this.t("errors.roleInUse"), 409);
1124
- }
1125
- return await this.roleRepository.delete(id);
1193
+ /**
1194
+ * Claim the previous-token grace slot for a single family. Conditional on
1195
+ * BOTH the stored previousHash still matching the presented token AND
1196
+ * previousUsedAt being NULL. Gating on the hash (not just the flag) closes
1197
+ * the rotation race: the winner's rotation rewrites previousHash via
1198
+ * storeRefreshToken (and resets previousUsedAt to NULL), so a loser whose
1199
+ * UPDATE lands after that rotation no longer matches and gets zero rows —
1200
+ * exactly one caller ever claims the slot.
1201
+ */
1202
+ async markPreviousUsed(tokenFamily, previousHash) {
1203
+ return await this.db.update(this.tokens).set({ previousUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and2(eq4(this.tokens.tokenFamily, tokenFamily), eq4(this.tokens.previousHash, previousHash), isNull2(this.tokens.previousUsedAt), eq4(this.tokens.status, "active"))).returning();
1126
1204
  }
1127
- async seedDefaultRoles(defaultRoles) {
1128
- const rolesToCreate = [];
1129
- for (const role of defaultRoles) {
1130
- const exists = await this.roleValidator.isRoleNameExists(role.name);
1131
- if (!exists) {
1132
- rolesToCreate.push(role);
1133
- }
1134
- }
1135
- const createdRoles = await Promise.all(rolesToCreate.map((role) => this.roleRepository.create(role)));
1136
- return createdRoles;
1205
+ /** Look up a single session's token row by its family identifier. */
1206
+ async getByFamily(tokenFamily) {
1207
+ const [token] = await this.db.select().from(this.tokens).where(and2(eq4(this.tokens.tokenFamily, tokenFamily), eq4(this.tokens.status, "active")));
1208
+ return token ?? null;
1137
1209
  }
1138
- async getRoleIdByName(name) {
1139
- const role = await this.getByName(name);
1140
- return role?.id;
1210
+ /**
1211
+ * Durably revoke one family without depending on physical deletion.
1212
+ *
1213
+ * The row remains as a tombstone until its original refresh expiry. This is
1214
+ * what prevents a later cache loss from turning a failed/omitted cleanup
1215
+ * delete into a valid database-backed recovery session.
1216
+ */
1217
+ async revokeFamily(tokenFamily) {
1218
+ return this.db.update(this.tokens).set({
1219
+ status: "revoked",
1220
+ previousHash: null,
1221
+ previousValidUntil: null,
1222
+ previousUsedAt: null
1223
+ }).where(and2(eq4(this.tokens.tokenFamily, tokenFamily), eq4(this.tokens.status, "active"))).returning();
1224
+ }
1225
+ /** Durably revoke every active family for a user. */
1226
+ async revokeAllForUser(userId) {
1227
+ return this.db.update(this.tokens).set({
1228
+ status: "revoked",
1229
+ previousHash: null,
1230
+ previousValidUntil: null,
1231
+ previousUsedAt: null
1232
+ }).where(and2(eq4(this.tokens.userId, userId), eq4(this.tokens.status, "active"))).returning();
1141
1233
  }
1142
1234
  /**
1143
- * checkNameUnique() reads before it writes, so two concurrent requests can
1144
- * both pass it and race to insert the same name. The database rejects the
1145
- * loser; answer it with the same 409 the validator would have given rather
1146
- * than leaking a raw driver error as a 500.
1235
+ * Opportunistic cleanup: with one row per family (no unique userId), expired
1236
+ * and abandoned sessions accumulate. Delete every expired row.
1147
1237
  */
1148
- async onDuplicateName(write) {
1149
- try {
1150
- return await write();
1151
- } catch (error) {
1152
- if (isDuplicateRoleName(error)) {
1153
- Err3(this.t("errors.exists"), 409);
1154
- }
1155
- throw error;
1156
- }
1238
+ async deleteExpired() {
1239
+ return this.db.delete(this.tokens).where(lt(this.tokens.expiresAt, (/* @__PURE__ */ new Date()).toISOString())).returning();
1240
+ }
1241
+ async isUserExists(userId) {
1242
+ const [user] = await this.db.select({ id: this.users.id }).from(this.users).where(eq4(this.users.id, userId)).limit(1);
1243
+ return !!user;
1244
+ }
1245
+ async getRoleNameById(userId) {
1246
+ return this.q.getRoleName(userId);
1247
+ }
1248
+ async getUserPermissions(userId) {
1249
+ return this.q.getUserPermissions(userId);
1250
+ }
1251
+ async getRoleAndPermissions(userId) {
1252
+ return this.q.getRoleAndPermissions(userId);
1253
+ }
1254
+ async getUser(userId) {
1255
+ return await this.q.getUserWithPermissions(eq4(this.users.id, userId)) ?? null;
1157
1256
  }
1158
1257
  };
1159
1258
  __decorate7([
1160
- I18n3("roles"),
1259
+ DB3(),
1161
1260
  __metadata7("design:type", Object)
1162
- ], RoleService.prototype, "t", void 0);
1163
- RoleService = __decorate7([
1164
- Injectable4(),
1165
- __metadata7("design:paramtypes", [typeof (_a4 = typeof RoleRepository !== "undefined" && RoleRepository) === "function" ? _a4 : Object, typeof (_b2 = typeof RoleValidator !== "undefined" && RoleValidator) === "function" ? _b2 : Object])
1166
- ], RoleService);
1167
- function isDuplicateRoleName(error) {
1168
- if (!error || typeof error !== "object")
1169
- return false;
1170
- const candidate = error;
1171
- if (candidate.code === "23505")
1172
- return true;
1173
- if (candidate.code === "SQLITE_CONSTRAINT_UNIQUE")
1174
- return true;
1175
- const message = typeof candidate.message === "string" ? candidate.message : "";
1176
- return /unique constraint failed/i.test(message) || /duplicate key value violates unique constraint/i.test(message);
1177
- }
1178
- __name(isDuplicateRoleName, "isDuplicateRoleName");
1179
-
1180
- // src/users/UserService.ts
1181
- import { nanoid as nanoid3 } from "nanoid";
1261
+ ], TokenRepository.prototype, "db", void 0);
1262
+ __decorate7([
1263
+ Inject5(AUTH_SCHEMA),
1264
+ __metadata7("design:type", Object)
1265
+ ], TokenRepository.prototype, "schema", void 0);
1266
+ TokenRepository = __decorate7([
1267
+ Repository3()
1268
+ ], TokenRepository);
1182
1269
 
1183
- // src/shared/index.ts
1184
- import * as fs from "fs/promises";
1185
- import * as path from "path";
1186
- import _isEmpty from "lodash.isempty";
1187
- import { Err as Err4 } from "najm-core";
1188
- var avatarsPath = path.join(process.cwd(), "avatars");
1189
- var parseSchema = /* @__PURE__ */ __name(async (schema, data) => {
1190
- try {
1191
- return await schema.parseAsync(data);
1192
- } catch (error) {
1193
- const errors = error.issues || error.errors || [];
1194
- const errorMessage = errors.map((err) => `${err.path.join(".")}: ${err.message}`).join("; ");
1195
- Err4(errorMessage);
1270
+ // src/tokens/SessionInvalidationService.ts
1271
+ var __decorate8 = function(decorators, target, key, desc) {
1272
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1273
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1274
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1275
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1276
+ };
1277
+ var __metadata8 = function(k, v) {
1278
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1279
+ };
1280
+ var SessionInvalidationService_1;
1281
+ var _a4;
1282
+ var _b2;
1283
+ var _c;
1284
+ var SessionInvalidationService = class SessionInvalidationService2 {
1285
+ static {
1286
+ __name(this, "SessionInvalidationService");
1196
1287
  }
1197
- }, "parseSchema");
1198
- var clean = /* @__PURE__ */ __name((obj) => {
1199
- const cleaned = {};
1200
- for (const [key, value] of Object.entries(obj)) {
1201
- if (value !== void 0 && value !== "") {
1202
- cleaned[key] = value;
1203
- }
1204
- }
1205
- return cleaned;
1206
- }, "clean");
1207
- var getAvatarFile = /* @__PURE__ */ __name(async (fileName) => {
1208
- try {
1209
- const filePath = path.join(avatarsPath, fileName);
1210
- const buffer = await fs.readFile(filePath);
1211
- const file = new File([buffer], fileName, {
1212
- type: "image/png"
1213
- });
1214
- return file;
1215
- } catch (error) {
1216
- return null;
1217
- }
1218
- }, "getAvatarFile");
1219
- var formatDate = /* @__PURE__ */ __name((dateValue) => {
1220
- if (!dateValue)
1221
- return null;
1222
- let date;
1223
- if (dateValue instanceof Date) {
1224
- date = dateValue;
1225
- } else if (typeof dateValue === "string") {
1226
- date = new Date(dateValue);
1227
- } else {
1228
- return null;
1229
- }
1230
- if (isNaN(date.getTime()))
1231
- return null;
1232
- return date.toISOString().split("T")[0];
1233
- }, "formatDate");
1234
- function calculateAge(dateOfBirth) {
1235
- if (!dateOfBirth)
1236
- return null;
1237
- const formattedDate = formatDate(dateOfBirth);
1238
- if (!formattedDate)
1239
- return null;
1240
- const birth = new Date(formattedDate);
1241
- const today = /* @__PURE__ */ new Date();
1242
- let age = today.getFullYear() - birth.getFullYear();
1243
- const monthDiff = today.getMonth() - birth.getMonth();
1244
- if (monthDiff < 0 || monthDiff === 0 && today.getDate() < birth.getDate()) {
1245
- age--;
1246
- }
1247
- return age;
1248
- }
1249
- __name(calculateAge, "calculateAge");
1250
- function calculateYearsOfExperience(hireDate) {
1251
- if (!hireDate)
1252
- return null;
1253
- const formattedDate = formatDate(hireDate);
1254
- if (!formattedDate)
1255
- return null;
1256
- const hire = new Date(formattedDate);
1257
- const today = /* @__PURE__ */ new Date();
1258
- let years = today.getFullYear() - hire.getFullYear();
1259
- const monthDiff = today.getMonth() - hire.getMonth();
1260
- if (monthDiff < 0 || monthDiff === 0 && today.getDate() < hire.getDate()) {
1261
- years--;
1262
- }
1263
- return years;
1264
- }
1265
- __name(calculateYearsOfExperience, "calculateYearsOfExperience");
1266
- function pickProps(source, keys) {
1267
- const result = {};
1268
- for (const key of keys) {
1269
- if (source[key] !== void 0) {
1270
- result[key] = source[key];
1271
- }
1272
- }
1273
- return result;
1274
- }
1275
- __name(pickProps, "pickProps");
1276
- var isEmpty = _isEmpty;
1277
- var isPath = /* @__PURE__ */ __name((img) => typeof img === "string" && img.trim().length > 0 && (img.startsWith("/") || img.startsWith("http") || img.startsWith("storage/")), "isPath");
1278
- var isFile = /* @__PURE__ */ __name((img) => !!img && typeof img !== "string" && img instanceof File, "isFile");
1279
-
1280
- // src/identity/presets.ts
1281
- var compactPhone = /* @__PURE__ */ __name((value) => value.trim().replace(/[\s().-]+/g, ""), "compactPhone");
1282
- var MOROCCO_LOCAL = /^0\d{9}$/;
1283
- var MOROCCO_NATIONAL = /^212\d{9}$/;
1284
- var MOROCCO_E164 = /^\+212\d{9}$/;
1285
- var normalizeMoroccanPhone = /* @__PURE__ */ __name((value) => {
1286
- const compact = compactPhone(value);
1287
- if (MOROCCO_LOCAL.test(compact))
1288
- return `+212${compact.slice(1)}`;
1289
- if (MOROCCO_NATIONAL.test(compact))
1290
- return `+${compact}`;
1291
- if (MOROCCO_E164.test(compact))
1292
- return compact;
1293
- return null;
1294
- }, "normalizeMoroccanPhone");
1295
- var moroccoIdentityPreset = {
1296
- name: "ma",
1297
- normalize: normalizeMoroccanPhone
1298
- };
1299
- var TUNISIA_LOCAL = /^\d{8}$/;
1300
- var TUNISIA_NATIONAL = /^216\d{8}$/;
1301
- var TUNISIA_E164 = /^\+216\d{8}$/;
1302
- var normalizeTunisianPhone = /* @__PURE__ */ __name((value) => {
1303
- const compact = compactPhone(value);
1304
- if (TUNISIA_LOCAL.test(compact))
1305
- return `+216${compact}`;
1306
- if (TUNISIA_NATIONAL.test(compact))
1307
- return `+${compact}`;
1308
- if (TUNISIA_E164.test(compact))
1309
- return compact;
1310
- return null;
1311
- }, "normalizeTunisianPhone");
1312
- var tunisiaIdentityPreset = {
1313
- name: "tn",
1314
- normalize: normalizeTunisianPhone
1315
- };
1316
- var IDENTITY_PRESETS = {
1317
- ma: moroccoIdentityPreset,
1318
- tn: tunisiaIdentityPreset
1319
- };
1320
- var DEFAULT_IDENTITY_PRESET = "ma";
1321
-
1322
- // src/identity/resolver.ts
1323
- var MAX_IDENTIFIER_LENGTH = 254;
1324
- var E164 = /^\+[1-9]\d{7,14}$/;
1325
- var normalizeE164 = /* @__PURE__ */ __name((value) => {
1326
- const compact = compactPhone(value);
1327
- const international = compact.startsWith("00") ? `+${compact.slice(2)}` : compact;
1328
- return E164.test(international) ? international : null;
1329
- }, "normalizeE164");
1330
- var selectPreset = /* @__PURE__ */ __name((preset) => {
1331
- if (preset === null)
1332
- return null;
1333
- if (preset === void 0)
1334
- return IDENTITY_PRESETS[DEFAULT_IDENTITY_PRESET];
1335
- if (typeof preset === "string") {
1336
- const selected = IDENTITY_PRESETS[preset];
1337
- if (!selected)
1338
- throw new Error(`auth.identity.preset '${preset}' is not a built-in identity preset`);
1339
- return selected;
1340
- }
1341
- if (typeof preset.normalize !== "function") {
1342
- throw new Error("auth.identity.preset must expose a normalize(value) function");
1343
- }
1344
- return preset;
1345
- }, "selectPreset");
1346
- function createIdentityResolver(config) {
1347
- const preset = selectPreset(config?.preset);
1348
- const extend = config?.extend ?? [];
1349
- if (extend.some((normalizer) => typeof normalizer !== "function")) {
1350
- throw new Error("auth.identity.extend must contain only normalizer functions");
1351
- }
1352
- const pipeline = [
1353
- ...extend,
1354
- ...preset ? [preset.normalize] : [],
1355
- normalizeE164
1356
- ];
1357
- return (value) => {
1358
- if (typeof value !== "string")
1359
- return null;
1360
- const trimmed = value.trim();
1361
- if (!trimmed || trimmed.length > MAX_IDENTIFIER_LENGTH)
1362
- return null;
1363
- if (trimmed.includes("@"))
1364
- return trimmed.toLowerCase();
1365
- for (const normalize of pipeline) {
1366
- const normalized = normalize(trimmed);
1367
- if (normalized)
1368
- return normalized;
1369
- }
1370
- return null;
1371
- };
1372
- }
1373
- __name(createIdentityResolver, "createIdentityResolver");
1374
- var defaultResolver = createIdentityResolver();
1375
- function normalizeAuthIdentifier(value) {
1376
- return defaultResolver(value);
1377
- }
1378
- __name(normalizeAuthIdentifier, "normalizeAuthIdentifier");
1379
- function isEmailIdentifier(value) {
1380
- return value.includes("@");
1381
- }
1382
- __name(isEmailIdentifier, "isEmailIdentifier");
1383
-
1384
- // src/users/UserService.ts
1385
- import { Err as Err5 } from "najm-core";
1386
-
1387
- // src/tokens/SessionInvalidationService.ts
1388
- import { Inject as Inject6, Injectable as Injectable5 } from "najm-core";
1389
- import { CacheService } from "najm-cache";
1390
- import timestring2 from "timestring";
1391
-
1392
- // src/tokens/TokenRepository.ts
1393
- import { and, eq as eq4, isNull, lt } from "drizzle-orm";
1394
- import { Repository as Repository3, Inject as Inject5 } from "najm-core";
1395
- import { DB as DB3 } from "najm-database";
1396
- var __decorate8 = function(decorators, target, key, desc) {
1397
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1398
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1399
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1400
- return c > 3 && r && Object.defineProperty(target, key, r), r;
1401
- };
1402
- var __metadata8 = function(k, v) {
1403
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1404
- };
1405
- var TokenRepository = class TokenRepository2 {
1406
- static {
1407
- __name(this, "TokenRepository");
1408
- }
1409
- db;
1410
- schema;
1411
- get tokens() {
1412
- return this.schema.tokens;
1413
- }
1414
- get users() {
1415
- return this.schema.users;
1416
- }
1417
- /** Shared query helper, scoped to the current database/transaction identity. */
1418
- queryHelper;
1419
- get q() {
1420
- const db = this.db;
1421
- if (this.queryHelper?.db !== db) {
1422
- this.queryHelper = { db, queries: new AuthQueries(db, this.schema) };
1423
- }
1424
- return this.queryHelper.queries;
1425
- }
1426
- /**
1427
- * Upsert the refresh-token row for a session, keyed on `tokenFamily` (the
1428
- * per-login session identifier, unique). A brand-new login inserts a fresh
1429
- * family row; a refresh rotation updates only that family's row, leaving the
1430
- * user's other sessions untouched.
1431
- */
1432
- async storeRefreshToken(tokenData) {
1433
- return await this.db.insert(this.tokens).values(tokenData).onConflictDoUpdate({
1434
- target: this.tokens.tokenFamily,
1435
- set: {
1436
- token: tokenData.token,
1437
- expiresAt: tokenData.expiresAt,
1438
- previousHash: tokenData.previousHash ?? null,
1439
- previousValidUntil: tokenData.previousValidUntil ?? null,
1440
- previousUsedAt: tokenData.previousUsedAt ?? null
1441
- },
1442
- // A revoked family is a durable tombstone. It must never be revived by
1443
- // an issuance racing with logout, or after the cache markers are lost.
1444
- setWhere: and(eq4(this.tokens.status, "active"), eq4(this.tokens.userId, tokenData.userId))
1445
- }).returning();
1446
- }
1447
- /**
1448
- * Rotate an existing refresh-token family with compare-and-swap semantics.
1449
- * This can never update a family durably revoked by a concurrent logout.
1450
- */
1451
- async rotateRefreshToken(tokenData, expectedCurrentHash) {
1452
- return await this.db.update(this.tokens).set({
1453
- token: tokenData.token,
1454
- expiresAt: tokenData.expiresAt,
1455
- previousHash: tokenData.previousHash,
1456
- previousValidUntil: tokenData.previousValidUntil,
1457
- previousUsedAt: tokenData.previousUsedAt ?? null
1458
- }).where(and(eq4(this.tokens.tokenFamily, tokenData.tokenFamily), eq4(this.tokens.userId, tokenData.userId), eq4(this.tokens.token, expectedCurrentHash), eq4(this.tokens.status, "active"))).returning();
1459
- }
1460
- /**
1461
- * Claim the previous-token grace slot for a single family. Conditional on
1462
- * BOTH the stored previousHash still matching the presented token AND
1463
- * previousUsedAt being NULL. Gating on the hash (not just the flag) closes
1464
- * the rotation race: the winner's rotation rewrites previousHash via
1465
- * storeRefreshToken (and resets previousUsedAt to NULL), so a loser whose
1466
- * UPDATE lands after that rotation no longer matches and gets zero rows —
1467
- * exactly one caller ever claims the slot.
1468
- */
1469
- async markPreviousUsed(tokenFamily, previousHash) {
1470
- return await this.db.update(this.tokens).set({ previousUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and(eq4(this.tokens.tokenFamily, tokenFamily), eq4(this.tokens.previousHash, previousHash), isNull(this.tokens.previousUsedAt), eq4(this.tokens.status, "active"))).returning();
1471
- }
1472
- /** Look up a single session's token row by its family identifier. */
1473
- async getByFamily(tokenFamily) {
1474
- const [token] = await this.db.select().from(this.tokens).where(and(eq4(this.tokens.tokenFamily, tokenFamily), eq4(this.tokens.status, "active")));
1475
- return token ?? null;
1476
- }
1477
- /**
1478
- * Durably revoke one family without depending on physical deletion.
1479
- *
1480
- * The row remains as a tombstone until its original refresh expiry. This is
1481
- * what prevents a later cache loss from turning a failed/omitted cleanup
1482
- * delete into a valid database-backed recovery session.
1483
- */
1484
- async revokeFamily(tokenFamily) {
1485
- return this.db.update(this.tokens).set({
1486
- status: "revoked",
1487
- previousHash: null,
1488
- previousValidUntil: null,
1489
- previousUsedAt: null
1490
- }).where(and(eq4(this.tokens.tokenFamily, tokenFamily), eq4(this.tokens.status, "active"))).returning();
1491
- }
1492
- /** Durably revoke every active family for a user. */
1493
- async revokeAllForUser(userId) {
1494
- return this.db.update(this.tokens).set({
1495
- status: "revoked",
1496
- previousHash: null,
1497
- previousValidUntil: null,
1498
- previousUsedAt: null
1499
- }).where(and(eq4(this.tokens.userId, userId), eq4(this.tokens.status, "active"))).returning();
1500
- }
1501
- /**
1502
- * Opportunistic cleanup: with one row per family (no unique userId), expired
1503
- * and abandoned sessions accumulate. Delete every expired row.
1504
- */
1505
- async deleteExpired() {
1506
- return this.db.delete(this.tokens).where(lt(this.tokens.expiresAt, (/* @__PURE__ */ new Date()).toISOString())).returning();
1507
- }
1508
- async isUserExists(userId) {
1509
- const [user] = await this.db.select({ id: this.users.id }).from(this.users).where(eq4(this.users.id, userId)).limit(1);
1510
- return !!user;
1511
- }
1512
- async getRoleNameById(userId) {
1513
- return this.q.getRoleName(userId);
1514
- }
1515
- async getUserPermissions(userId) {
1516
- return this.q.getUserPermissions(userId);
1517
- }
1518
- async getRoleAndPermissions(userId) {
1519
- return this.q.getRoleAndPermissions(userId);
1520
- }
1521
- async getUser(userId) {
1522
- return await this.q.getUserWithPermissions(eq4(this.users.id, userId)) ?? null;
1523
- }
1524
- };
1525
- __decorate8([
1526
- DB3(),
1527
- __metadata8("design:type", Object)
1528
- ], TokenRepository.prototype, "db", void 0);
1529
- __decorate8([
1530
- Inject5(AUTH_SCHEMA),
1531
- __metadata8("design:type", Object)
1532
- ], TokenRepository.prototype, "schema", void 0);
1533
- TokenRepository = __decorate8([
1534
- Repository3()
1535
- ], TokenRepository);
1536
-
1537
- // src/tokens/SessionInvalidationService.ts
1538
- var __decorate9 = function(decorators, target, key, desc) {
1539
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1540
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1541
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1542
- return c > 3 && r && Object.defineProperty(target, key, r), r;
1543
- };
1544
- var __metadata9 = function(k, v) {
1545
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1546
- };
1547
- var SessionInvalidationService_1;
1548
- var _a5;
1549
- var _b3;
1550
- var SessionInvalidationService = class SessionInvalidationService2 {
1551
- static {
1552
- __name(this, "SessionInvalidationService");
1553
- }
1554
- static {
1555
- SessionInvalidationService_1 = this;
1288
+ static {
1289
+ SessionInvalidationService_1 = this;
1556
1290
  }
1557
1291
  cache;
1558
1292
  tokens;
1293
+ transactions;
1559
1294
  config;
1560
- constructor(cache2, tokens) {
1295
+ constructor(cache2, tokens, transactions) {
1561
1296
  this.cache = cache2;
1562
1297
  this.tokens = tokens;
1298
+ this.transactions = transactions;
1563
1299
  }
1564
1300
  /**
1565
1301
  * Fields whose change ends existing sessions. Everything absent from this set
@@ -1582,7 +1318,7 @@ var SessionInvalidationService = class SessionInvalidationService2 {
1582
1318
  return SessionInvalidationService_1.SECURITY_FIELDS.some((field) => data[field] !== void 0);
1583
1319
  }
1584
1320
  get accessTokenTtlMs() {
1585
- return timestring2(this.config.jwt.accessExpiresIn, "ms");
1321
+ return Math.max(timestring2(this.config.jwt.accessExpiresIn, "ms"), (this.config.session?.maxAge ?? 300) * 1e3);
1586
1322
  }
1587
1323
  get refreshTokenTtlMs() {
1588
1324
  return timestring2(this.config.jwt.refreshExpiresIn, "ms");
@@ -1636,9 +1372,15 @@ var SessionInvalidationService = class SessionInvalidationService2 {
1636
1372
  * the same version and write the same successor back.
1637
1373
  */
1638
1374
  async invalidateAccessTokens(userId) {
1375
+ const version = await this.invalidateAccessTokensNow(userId);
1376
+ await this.afterTransaction(() => this.invalidateAccessTokens(userId));
1377
+ return version;
1378
+ }
1379
+ async invalidateAccessTokensNow(userId) {
1639
1380
  const key = this.sessionVersionKey(userId);
1640
1381
  const { count } = await this.cache.incr(key, this.accessTokenTtlMs);
1641
1382
  await this.cache.expire(key, this.accessTokenTtlMs);
1383
+ await this.cache.del(`${this.userCacheKey(userId)}:${count - 1}`);
1642
1384
  await this.dropUserCache(userId);
1643
1385
  return count;
1644
1386
  }
@@ -1649,15 +1391,30 @@ var SessionInvalidationService = class SessionInvalidationService2 {
1649
1391
  * End every session a user holds: access tokens by version, refresh sessions
1650
1392
  * by row, and each family's liveness marker.
1651
1393
  *
1652
- * Callers run this AFTER their database mutation has committed. Running it
1653
- * before would leave a window in which a concurrent login re-established a
1654
- * session against the state the mutation was about to remove; running it
1655
- * after a rollback merely signs the user out again, which is safe.
1394
+ * Repeat invalidation when a caller-owned transaction completes. Readers can
1395
+ * recover from the previous committed state before commit, or from transient
1396
+ * state on a shared connection before rollback. Neither snapshot may survive.
1656
1397
  */
1657
1398
  async invalidateUser(userId) {
1658
- await this.invalidateAccessTokens(userId);
1399
+ await this.invalidateAccessTokensNow(userId);
1400
+ await this.revokeAllForUserNow(userId);
1401
+ await this.afterTransaction(() => this.invalidateUser(userId));
1402
+ }
1403
+ async revokeAllForUser(userId) {
1404
+ const revoked = await this.revokeAllForUserNow(userId);
1405
+ await this.afterTransaction(() => this.revokeAllForUser(userId));
1406
+ return revoked;
1407
+ }
1408
+ async afterTransaction(callback) {
1409
+ if (!this.transactions?.isActive())
1410
+ return;
1411
+ await this.transactions.afterCommit(callback);
1412
+ await this.transactions.afterRollback(callback);
1413
+ }
1414
+ async revokeAllForUserNow(userId) {
1659
1415
  const revoked = await this.tokens.revokeAllForUser(userId);
1660
1416
  await Promise.all(familiesOf(revoked).map((family) => this.markFamilyRevoked(family)));
1417
+ return revoked;
1661
1418
  }
1662
1419
  /**
1663
1420
  * Record that a family is live, and whose it is. Called wherever the family's
@@ -1722,40 +1479,365 @@ var SessionInvalidationService = class SessionInvalidationService2 {
1722
1479
  async isFamilyLive(tokenFamily, userId) {
1723
1480
  return await this.familyStatus(tokenFamily, userId) === "live";
1724
1481
  }
1725
- async readMany(keys) {
1726
- const cache2 = this.cache;
1727
- if (cache2.getMany)
1728
- return cache2.getMany(keys);
1729
- return Promise.all(keys.map((key) => cache2.get(key)));
1482
+ async readMany(keys) {
1483
+ const cache2 = this.cache;
1484
+ if (cache2.getMany)
1485
+ return cache2.getMany(keys);
1486
+ return Promise.all(keys.map((key) => cache2.get(key)));
1487
+ }
1488
+ /**
1489
+ * Keep revocation through both credential lifetimes. If deleting the refresh
1490
+ * row fails, its still-valid cookie must remain denied after access expires.
1491
+ */
1492
+ async markFamilyRevoked(tokenFamily) {
1493
+ await this.cache.set(this.revokedFamilyKey(tokenFamily), "1", Math.max(this.accessTokenTtlMs, this.refreshTokenTtlMs));
1494
+ await this.cache.del(this.familyKey(tokenFamily));
1495
+ }
1496
+ async isFamilyRevoked(tokenFamily) {
1497
+ return await this.cache.get(this.revokedFamilyKey(tokenFamily)) !== null;
1498
+ }
1499
+ };
1500
+ __decorate8([
1501
+ Inject6(AUTH_CONFIG),
1502
+ __metadata8("design:type", Object)
1503
+ ], SessionInvalidationService.prototype, "config", void 0);
1504
+ SessionInvalidationService = SessionInvalidationService_1 = __decorate8([
1505
+ Injectable4(),
1506
+ __metadata8("design:paramtypes", [typeof (_a4 = typeof CacheService !== "undefined" && CacheService) === "function" ? _a4 : Object, typeof (_b2 = typeof TokenRepository !== "undefined" && TokenRepository) === "function" ? _b2 : Object, typeof (_c = typeof TransactionService !== "undefined" && TransactionService) === "function" ? _c : Object])
1507
+ ], SessionInvalidationService);
1508
+ function familiesOf(rows) {
1509
+ if (!Array.isArray(rows))
1510
+ return [];
1511
+ return rows.map((row) => row?.tokenFamily).filter((family) => typeof family === "string" && family.length > 0);
1512
+ }
1513
+ __name(familiesOf, "familiesOf");
1514
+
1515
+ // src/roles/RoleService.ts
1516
+ var __decorate9 = function(decorators, target, key, desc) {
1517
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1518
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1519
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1520
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1521
+ };
1522
+ var __metadata9 = function(k, v) {
1523
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1524
+ };
1525
+ var _a5;
1526
+ var _b3;
1527
+ var _c2;
1528
+ var _d;
1529
+ var RoleService = class RoleService2 {
1530
+ static {
1531
+ __name(this, "RoleService");
1532
+ }
1533
+ roleRepository;
1534
+ roleValidator;
1535
+ userRepository;
1536
+ sessionInvalidation;
1537
+ t;
1538
+ constructor(roleRepository, roleValidator, userRepository, sessionInvalidation) {
1539
+ this.roleRepository = roleRepository;
1540
+ this.roleValidator = roleValidator;
1541
+ this.userRepository = userRepository;
1542
+ this.sessionInvalidation = sessionInvalidation;
1543
+ }
1544
+ async getAll() {
1545
+ return await this.roleRepository.getAll();
1546
+ }
1547
+ async getById(id) {
1548
+ await this.roleValidator.checkRoleExists(id);
1549
+ return await this.roleRepository.getById(id);
1550
+ }
1551
+ async getByName(name) {
1552
+ return await this.roleRepository.getByName(name);
1553
+ }
1554
+ async create(data) {
1555
+ await this.roleValidator.checkNameUnique(data.name);
1556
+ return await this.onDuplicateName(() => this.roleRepository.create(data));
1557
+ }
1558
+ async update(id, data) {
1559
+ const role = await this.roleValidator.checkRoleExists(id);
1560
+ if (role.name === ROLES.ADMIN && data.name && data.name !== ROLES.ADMIN) {
1561
+ Err3(this.t("errors.cannotRenameSystem"), 403);
1562
+ }
1563
+ await this.roleValidator.checkNameUnique(data.name, id);
1564
+ const updated = await this.onDuplicateName(() => this.roleRepository.update(id, data));
1565
+ if (data.name !== void 0 && this.userRepository && this.sessionInvalidation) {
1566
+ for (const userId of await this.userRepository.getIdsByRole(id)) {
1567
+ await this.sessionInvalidation.invalidateAccessTokens(userId);
1568
+ }
1569
+ }
1570
+ return updated;
1571
+ }
1572
+ async delete(id) {
1573
+ const role = await this.roleValidator.checkRoleExists(id);
1574
+ if (role.name === ROLES.ADMIN) {
1575
+ Err3(this.t("errors.cannotDeleteSystem"), 403);
1576
+ }
1577
+ if (await this.roleRepository.hasUsers(id)) {
1578
+ Err3(this.t("errors.roleInUse"), 409);
1579
+ }
1580
+ return await this.roleRepository.delete(id);
1581
+ }
1582
+ async seedDefaultRoles(defaultRoles) {
1583
+ const rolesToCreate = [];
1584
+ for (const role of defaultRoles) {
1585
+ const exists = await this.roleValidator.isRoleNameExists(role.name);
1586
+ if (!exists) {
1587
+ rolesToCreate.push(role);
1588
+ }
1589
+ }
1590
+ const createdRoles = await Promise.all(rolesToCreate.map((role) => this.roleRepository.create(role)));
1591
+ return createdRoles;
1592
+ }
1593
+ async getRoleIdByName(name) {
1594
+ const role = await this.getByName(name);
1595
+ return role?.id;
1596
+ }
1597
+ /**
1598
+ * checkNameUnique() reads before it writes, so two concurrent requests can
1599
+ * both pass it and race to insert the same name. The database rejects the
1600
+ * loser; answer it with the same 409 the validator would have given rather
1601
+ * than leaking a raw driver error as a 500.
1602
+ */
1603
+ async onDuplicateName(write) {
1604
+ try {
1605
+ return await write();
1606
+ } catch (error) {
1607
+ if (isDuplicateRoleName(error)) {
1608
+ Err3(this.t("errors.exists"), 409);
1609
+ }
1610
+ throw error;
1611
+ }
1612
+ }
1613
+ };
1614
+ __decorate9([
1615
+ I18n3("roles"),
1616
+ __metadata9("design:type", Object)
1617
+ ], RoleService.prototype, "t", void 0);
1618
+ RoleService = __decorate9([
1619
+ Injectable5(),
1620
+ __metadata9("design:paramtypes", [typeof (_a5 = typeof RoleRepository !== "undefined" && RoleRepository) === "function" ? _a5 : Object, typeof (_b3 = typeof RoleValidator !== "undefined" && RoleValidator) === "function" ? _b3 : Object, typeof (_c2 = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _c2 : Object, typeof (_d = typeof SessionInvalidationService !== "undefined" && SessionInvalidationService) === "function" ? _d : Object])
1621
+ ], RoleService);
1622
+ function isDuplicateRoleName(error) {
1623
+ if (!error || typeof error !== "object")
1624
+ return false;
1625
+ const candidate = error;
1626
+ if (candidate.code === "23505")
1627
+ return true;
1628
+ if (candidate.code === "SQLITE_CONSTRAINT_UNIQUE")
1629
+ return true;
1630
+ const message = typeof candidate.message === "string" ? candidate.message : "";
1631
+ return /unique constraint failed/i.test(message) || /duplicate key value violates unique constraint/i.test(message);
1632
+ }
1633
+ __name(isDuplicateRoleName, "isDuplicateRoleName");
1634
+
1635
+ // src/users/UserService.ts
1636
+ import { nanoid as nanoid3 } from "nanoid";
1637
+
1638
+ // src/shared/index.ts
1639
+ import * as fs from "fs/promises";
1640
+ import * as path from "path";
1641
+ import _isEmpty from "lodash.isempty";
1642
+ import { Err as Err4 } from "najm-core";
1643
+ var avatarsPath = path.join(process.cwd(), "avatars");
1644
+ var parseSchema = /* @__PURE__ */ __name(async (schema, data) => {
1645
+ try {
1646
+ return await schema.parseAsync(data);
1647
+ } catch (error) {
1648
+ const errors = error.issues || error.errors || [];
1649
+ const errorMessage = errors.map((err) => `${err.path.join(".")}: ${err.message}`).join("; ");
1650
+ Err4(errorMessage);
1651
+ }
1652
+ }, "parseSchema");
1653
+ var clean = /* @__PURE__ */ __name((obj) => {
1654
+ const cleaned = {};
1655
+ for (const [key, value] of Object.entries(obj)) {
1656
+ if (value !== void 0 && value !== "") {
1657
+ cleaned[key] = value;
1658
+ }
1659
+ }
1660
+ return cleaned;
1661
+ }, "clean");
1662
+ var getAvatarFile = /* @__PURE__ */ __name(async (fileName) => {
1663
+ try {
1664
+ const filePath = path.join(avatarsPath, fileName);
1665
+ const buffer = await fs.readFile(filePath);
1666
+ const file = new File([buffer], fileName, {
1667
+ type: "image/png"
1668
+ });
1669
+ return file;
1670
+ } catch (error) {
1671
+ return null;
1672
+ }
1673
+ }, "getAvatarFile");
1674
+ var formatDate = /* @__PURE__ */ __name((dateValue) => {
1675
+ if (!dateValue)
1676
+ return null;
1677
+ let date;
1678
+ if (dateValue instanceof Date) {
1679
+ date = dateValue;
1680
+ } else if (typeof dateValue === "string") {
1681
+ date = new Date(dateValue);
1682
+ } else {
1683
+ return null;
1684
+ }
1685
+ if (isNaN(date.getTime()))
1686
+ return null;
1687
+ return date.toISOString().split("T")[0];
1688
+ }, "formatDate");
1689
+ function calculateAge(dateOfBirth) {
1690
+ if (!dateOfBirth)
1691
+ return null;
1692
+ const formattedDate = formatDate(dateOfBirth);
1693
+ if (!formattedDate)
1694
+ return null;
1695
+ const birth = new Date(formattedDate);
1696
+ const today = /* @__PURE__ */ new Date();
1697
+ let age = today.getFullYear() - birth.getFullYear();
1698
+ const monthDiff = today.getMonth() - birth.getMonth();
1699
+ if (monthDiff < 0 || monthDiff === 0 && today.getDate() < birth.getDate()) {
1700
+ age--;
1701
+ }
1702
+ return age;
1703
+ }
1704
+ __name(calculateAge, "calculateAge");
1705
+ function calculateYearsOfExperience(hireDate) {
1706
+ if (!hireDate)
1707
+ return null;
1708
+ const formattedDate = formatDate(hireDate);
1709
+ if (!formattedDate)
1710
+ return null;
1711
+ const hire = new Date(formattedDate);
1712
+ const today = /* @__PURE__ */ new Date();
1713
+ let years = today.getFullYear() - hire.getFullYear();
1714
+ const monthDiff = today.getMonth() - hire.getMonth();
1715
+ if (monthDiff < 0 || monthDiff === 0 && today.getDate() < hire.getDate()) {
1716
+ years--;
1717
+ }
1718
+ return years;
1719
+ }
1720
+ __name(calculateYearsOfExperience, "calculateYearsOfExperience");
1721
+ function pickProps(source, keys) {
1722
+ const result = {};
1723
+ for (const key of keys) {
1724
+ if (source[key] !== void 0) {
1725
+ result[key] = source[key];
1726
+ }
1727
+ }
1728
+ return result;
1729
+ }
1730
+ __name(pickProps, "pickProps");
1731
+ var isEmpty = _isEmpty;
1732
+ var isPath = /* @__PURE__ */ __name((img) => typeof img === "string" && img.trim().length > 0 && (img.startsWith("/") || img.startsWith("http") || img.startsWith("storage/")), "isPath");
1733
+ var isFile = /* @__PURE__ */ __name((img) => !!img && typeof img !== "string" && img instanceof File, "isFile");
1734
+
1735
+ // src/identity/presets.ts
1736
+ var compactPhone = /* @__PURE__ */ __name((value) => value.trim().replace(/[\s().-]+/g, ""), "compactPhone");
1737
+ var MOROCCO_LOCAL = /^0\d{9}$/;
1738
+ var MOROCCO_NATIONAL = /^212\d{9}$/;
1739
+ var MOROCCO_E164 = /^\+212\d{9}$/;
1740
+ var normalizeMoroccanPhone = /* @__PURE__ */ __name((value) => {
1741
+ const compact = compactPhone(value);
1742
+ if (MOROCCO_LOCAL.test(compact))
1743
+ return `+212${compact.slice(1)}`;
1744
+ if (MOROCCO_NATIONAL.test(compact))
1745
+ return `+${compact}`;
1746
+ if (MOROCCO_E164.test(compact))
1747
+ return compact;
1748
+ return null;
1749
+ }, "normalizeMoroccanPhone");
1750
+ var moroccoIdentityPreset = {
1751
+ name: "ma",
1752
+ normalize: normalizeMoroccanPhone
1753
+ };
1754
+ var TUNISIA_LOCAL = /^\d{8}$/;
1755
+ var TUNISIA_NATIONAL = /^216\d{8}$/;
1756
+ var TUNISIA_E164 = /^\+216\d{8}$/;
1757
+ var normalizeTunisianPhone = /* @__PURE__ */ __name((value) => {
1758
+ const compact = compactPhone(value);
1759
+ if (TUNISIA_LOCAL.test(compact))
1760
+ return `+216${compact}`;
1761
+ if (TUNISIA_NATIONAL.test(compact))
1762
+ return `+${compact}`;
1763
+ if (TUNISIA_E164.test(compact))
1764
+ return compact;
1765
+ return null;
1766
+ }, "normalizeTunisianPhone");
1767
+ var tunisiaIdentityPreset = {
1768
+ name: "tn",
1769
+ normalize: normalizeTunisianPhone
1770
+ };
1771
+ var IDENTITY_PRESETS = {
1772
+ ma: moroccoIdentityPreset,
1773
+ tn: tunisiaIdentityPreset
1774
+ };
1775
+ var DEFAULT_IDENTITY_PRESET = "ma";
1776
+
1777
+ // src/identity/resolver.ts
1778
+ var MAX_IDENTIFIER_LENGTH = 254;
1779
+ var E164 = /^\+[1-9]\d{7,14}$/;
1780
+ var normalizeE164 = /* @__PURE__ */ __name((value) => {
1781
+ const compact = compactPhone(value);
1782
+ const international = compact.startsWith("00") ? `+${compact.slice(2)}` : compact;
1783
+ return E164.test(international) ? international : null;
1784
+ }, "normalizeE164");
1785
+ var selectPreset = /* @__PURE__ */ __name((preset) => {
1786
+ if (preset === null)
1787
+ return null;
1788
+ if (preset === void 0)
1789
+ return IDENTITY_PRESETS[DEFAULT_IDENTITY_PRESET];
1790
+ if (typeof preset === "string") {
1791
+ const selected = IDENTITY_PRESETS[preset];
1792
+ if (!selected)
1793
+ throw new Error(`auth.identity.preset '${preset}' is not a built-in identity preset`);
1794
+ return selected;
1730
1795
  }
1731
- /**
1732
- * Keep revocation through both credential lifetimes. If deleting the refresh
1733
- * row fails, its still-valid cookie must remain denied after access expires.
1734
- */
1735
- async markFamilyRevoked(tokenFamily) {
1736
- await this.cache.set(this.revokedFamilyKey(tokenFamily), "1", Math.max(this.accessTokenTtlMs, this.refreshTokenTtlMs));
1737
- await this.cache.del(this.familyKey(tokenFamily));
1796
+ if (typeof preset.normalize !== "function") {
1797
+ throw new Error("auth.identity.preset must expose a normalize(value) function");
1738
1798
  }
1739
- async isFamilyRevoked(tokenFamily) {
1740
- return await this.cache.get(this.revokedFamilyKey(tokenFamily)) !== null;
1799
+ return preset;
1800
+ }, "selectPreset");
1801
+ function createIdentityResolver(config) {
1802
+ const preset = selectPreset(config?.preset);
1803
+ const extend = config?.extend ?? [];
1804
+ if (extend.some((normalizer) => typeof normalizer !== "function")) {
1805
+ throw new Error("auth.identity.extend must contain only normalizer functions");
1741
1806
  }
1742
- };
1743
- __decorate9([
1744
- Inject6(AUTH_CONFIG),
1745
- __metadata9("design:type", Object)
1746
- ], SessionInvalidationService.prototype, "config", void 0);
1747
- SessionInvalidationService = SessionInvalidationService_1 = __decorate9([
1748
- Injectable5(),
1749
- __metadata9("design:paramtypes", [typeof (_a5 = typeof CacheService !== "undefined" && CacheService) === "function" ? _a5 : Object, typeof (_b3 = typeof TokenRepository !== "undefined" && TokenRepository) === "function" ? _b3 : Object])
1750
- ], SessionInvalidationService);
1751
- function familiesOf(rows) {
1752
- if (!Array.isArray(rows))
1753
- return [];
1754
- return rows.map((row) => row?.tokenFamily).filter((family) => typeof family === "string" && family.length > 0);
1807
+ const pipeline = [
1808
+ ...extend,
1809
+ ...preset ? [preset.normalize] : [],
1810
+ normalizeE164
1811
+ ];
1812
+ return (value) => {
1813
+ if (typeof value !== "string")
1814
+ return null;
1815
+ const trimmed = value.trim();
1816
+ if (!trimmed || trimmed.length > MAX_IDENTIFIER_LENGTH)
1817
+ return null;
1818
+ if (trimmed.includes("@"))
1819
+ return trimmed.toLowerCase();
1820
+ for (const normalize of pipeline) {
1821
+ const normalized = normalize(trimmed);
1822
+ if (normalized)
1823
+ return normalized;
1824
+ }
1825
+ return null;
1826
+ };
1755
1827
  }
1756
- __name(familiesOf, "familiesOf");
1828
+ __name(createIdentityResolver, "createIdentityResolver");
1829
+ var defaultResolver = createIdentityResolver();
1830
+ function normalizeAuthIdentifier(value) {
1831
+ return defaultResolver(value);
1832
+ }
1833
+ __name(normalizeAuthIdentifier, "normalizeAuthIdentifier");
1834
+ function isEmailIdentifier(value) {
1835
+ return value.includes("@");
1836
+ }
1837
+ __name(isEmailIdentifier, "isEmailIdentifier");
1757
1838
 
1758
1839
  // src/users/UserService.ts
1840
+ import { Err as Err5 } from "najm-core";
1759
1841
  var __decorate10 = function(decorators, target, key, desc) {
1760
1842
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1761
1843
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1772,8 +1854,8 @@ var __param2 = function(paramIndex, decorator) {
1772
1854
  };
1773
1855
  var _a6;
1774
1856
  var _b4;
1775
- var _c;
1776
- var _d;
1857
+ var _c3;
1858
+ var _d2;
1777
1859
  var _e;
1778
1860
  var _f;
1779
1861
  var _g;
@@ -1905,7 +1987,7 @@ var UserService = class UserService2 {
1905
1987
  const userDetails = {
1906
1988
  id: userId,
1907
1989
  name: name || null,
1908
- email: email2,
1990
+ email: email2.trim().toLowerCase(),
1909
1991
  image,
1910
1992
  password: hashedPassword,
1911
1993
  roleId: resolvedRoleId,
@@ -1913,7 +1995,7 @@ var UserService = class UserService2 {
1913
1995
  ...normalizedPhone ? { phone: normalizedPhone } : {},
1914
1996
  status: data.status ?? this.authConfig.registrationMode ?? "active"
1915
1997
  };
1916
- const newUser = await this.userRepository.create(userDetails);
1998
+ const newUser = await this.userValidator.writeUnique(() => this.userRepository.create(userDetails));
1917
1999
  return this.sanitizeUser(newUser);
1918
2000
  }
1919
2001
  /**
@@ -1939,8 +2021,19 @@ var UserService = class UserService2 {
1939
2021
  image,
1940
2022
  ...hashedPassword && { password: hashedPassword }
1941
2023
  };
2024
+ if (typeof data.email === "string")
2025
+ updateData.email = data.email.trim().toLowerCase();
2026
+ if (data.phone !== void 0 && data.phone !== null) {
2027
+ const phone = this.authConfig.identity?.resolve(data.phone) ?? normalizeAuthIdentifier(data.phone);
2028
+ if (!phone || !E164_PHONE.test(phone))
2029
+ Err5("A valid phone number is required", 400);
2030
+ await this.userValidator.checkPhoneUnique(phone, id);
2031
+ updateData.phone = phone;
2032
+ }
1942
2033
  const cleanedUpdateData = clean(updateData);
1943
- const updatedUser = await this.userRepository.update(id, cleanedUpdateData);
2034
+ const updatedUser = await this.userValidator.writeUnique(() => options.expectedCredential ? this.userRepository.updateWithCredential(id, cleanedUpdateData, options.expectedCredential) : this.userRepository.update(id, cleanedUpdateData));
2035
+ if (options.expectedCredential)
2036
+ this.userValidator.checkCredentialUnchanged(updatedUser);
1944
2037
  const result = this.sanitizeUser(this.requireUser(updatedUser));
1945
2038
  if (SessionInvalidationService.affectsSecurityState(cleanedUpdateData)) {
1946
2039
  await this.invalidateSessions(id);
@@ -2033,20 +2126,20 @@ __decorate10([
2033
2126
  UserService = __decorate10([
2034
2127
  Injectable6(),
2035
2128
  __param2(6, Inject7(AUTH_CONFIG)),
2036
- __metadata10("design:paramtypes", [typeof (_a6 = typeof RoleValidator !== "undefined" && RoleValidator) === "function" ? _a6 : Object, typeof (_b4 = typeof RoleService !== "undefined" && RoleService) === "function" ? _b4 : Object, typeof (_c = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _c : Object, typeof (_d = typeof UserValidator !== "undefined" && UserValidator) === "function" ? _d : Object, typeof (_e = typeof EncryptionService !== "undefined" && EncryptionService) === "function" ? _e : Object, typeof (_f = typeof I18nService !== "undefined" && I18nService) === "function" ? _f : Object, Object, typeof (_g = typeof SessionInvalidationService !== "undefined" && SessionInvalidationService) === "function" ? _g : Object])
2129
+ __metadata10("design:paramtypes", [typeof (_a6 = typeof RoleValidator !== "undefined" && RoleValidator) === "function" ? _a6 : Object, typeof (_b4 = typeof RoleService !== "undefined" && RoleService) === "function" ? _b4 : Object, typeof (_c3 = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _c3 : Object, typeof (_d2 = typeof UserValidator !== "undefined" && UserValidator) === "function" ? _d2 : Object, typeof (_e = typeof EncryptionService !== "undefined" && EncryptionService) === "function" ? _e : Object, typeof (_f = typeof I18nService !== "undefined" && I18nService) === "function" ? _f : Object, Object, typeof (_g = typeof SessionInvalidationService !== "undefined" && SessionInvalidationService) === "function" ? _g : Object])
2037
2130
  ], UserService);
2038
2131
 
2039
2132
  // src/tokens/TokenService.ts
2040
2133
  import { Injectable as Injectable7, Inject as Inject9 } from "najm-core";
2041
2134
  import { I18n as I18n5 } from "najm-i18n";
2042
2135
  import { CacheService as CacheService2 } from "najm-cache";
2043
- import { createHash, createHmac } from "crypto";
2136
+ import { createHash, createHmac as createHmac2 } from "crypto";
2044
2137
  import jwt from "jsonwebtoken";
2045
2138
  import { nanoid as nanoid4 } from "nanoid";
2046
2139
  import timestring3 from "timestring";
2047
2140
 
2048
2141
  // src/credentialSetup/CredentialSetupRequirementRepository.ts
2049
- import { and as and2, eq as eq5 } from "drizzle-orm";
2142
+ import { and as and3, eq as eq5 } from "drizzle-orm";
2050
2143
  import { Err as Err6, Inject as Inject8, Repository as Repository4 } from "najm-core";
2051
2144
  import { DB as DB4 } from "najm-database";
2052
2145
  var __decorate11 = function(decorators, target, key, desc) {
@@ -2090,16 +2183,16 @@ var CredentialSetupRequirementRepository = class CredentialSetupRequirementRepos
2090
2183
  return requirement;
2091
2184
  }
2092
2185
  async find(userId, purpose) {
2093
- const [requirement] = await this.db.select(this.columns).from(this.requirements).where(and2(eq5(this.requirements.userId, userId), eq5(this.requirements.purpose, purpose))).limit(1);
2186
+ const [requirement] = await this.db.select(this.columns).from(this.requirements).where(and3(eq5(this.requirements.userId, userId), eq5(this.requirements.purpose, purpose))).limit(1);
2094
2187
  return requirement;
2095
2188
  }
2096
2189
  async listRequired(userId) {
2097
- return this.db.select(this.columns).from(this.requirements).where(and2(eq5(this.requirements.userId, userId), eq5(this.requirements.required, true)));
2190
+ return this.db.select(this.columns).from(this.requirements).where(and3(eq5(this.requirements.userId, userId), eq5(this.requirements.required, true)));
2098
2191
  }
2099
2192
  /** Only a still-required row completes, so a replayed completion is a no-op. */
2100
2193
  async complete(userId, purpose) {
2101
2194
  const now = (/* @__PURE__ */ new Date()).toISOString();
2102
- const [requirement] = await this.db.update(this.requirements).set({ required: false, completedAt: now, updatedAt: now }).where(and2(eq5(this.requirements.userId, userId), eq5(this.requirements.purpose, purpose), eq5(this.requirements.required, true))).returning(this.columns);
2195
+ const [requirement] = await this.db.update(this.requirements).set({ required: false, completedAt: now, updatedAt: now }).where(and3(eq5(this.requirements.userId, userId), eq5(this.requirements.purpose, purpose), eq5(this.requirements.required, true))).returning(this.columns);
2103
2196
  return requirement;
2104
2197
  }
2105
2198
  };
@@ -2138,6 +2231,15 @@ __name(storedTimeMs, "storedTimeMs");
2138
2231
 
2139
2232
  // src/tokens/TokenService.ts
2140
2233
  import { Err as Err7 } from "najm-core";
2234
+
2235
+ // src/auth/credentialState.ts
2236
+ import { createHmac } from "crypto";
2237
+ function credentialFingerprint(secret, user) {
2238
+ return createHmac("sha256", secret).update("najm:set-password:v1:").update(JSON.stringify([user.id, user.password, user.email, user.status, user.emailVerified])).digest("base64url");
2239
+ }
2240
+ __name(credentialFingerprint, "credentialFingerprint");
2241
+
2242
+ // src/tokens/TokenService.ts
2141
2243
  var __decorate12 = function(decorators, target, key, desc) {
2142
2244
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2143
2245
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2150,8 +2252,8 @@ var __metadata12 = function(k, v) {
2150
2252
  var TokenService_1;
2151
2253
  var _a7;
2152
2254
  var _b5;
2153
- var _c2;
2154
- var _d2;
2255
+ var _c4;
2256
+ var _d3;
2155
2257
  var _e2;
2156
2258
  var TokenService = class TokenService2 {
2157
2259
  static {
@@ -2220,10 +2322,13 @@ var TokenService = class TokenService2 {
2220
2322
  async verifyAccessToken(token) {
2221
2323
  let payload;
2222
2324
  try {
2223
- payload = jwt.verify(token, this.config.jwt.accessSecret);
2325
+ payload = jwt.verify(token, this.config.jwt.accessSecret, { algorithms: ["HS256"] });
2224
2326
  } catch {
2225
2327
  Err7(this.t("errors.tokenVerificationFailed"), 401);
2226
2328
  }
2329
+ if (!payload || typeof payload !== "object" || "type" in payload && payload.type !== "access" || typeof payload.userId !== "string" || !payload.userId || typeof payload.tokenFamily !== "string" || !payload.tokenFamily || !Number.isFinite(payload.exp) || payload.sessionVersion !== void 0 && (!Number.isSafeInteger(payload.sessionVersion) || payload.sessionVersion < 0)) {
2330
+ Err7(this.t("errors.tokenVerificationFailed"), 401);
2331
+ }
2227
2332
  const sessionKey = this.sessionVersionKey(payload.userId);
2228
2333
  const blacklistKey = payload.jti ? `${this.blacklistPrefix}${payload.jti}` : null;
2229
2334
  const revokedKey = payload.tokenFamily ? this.revokedFamilyKey(payload.tokenFamily) : null;
@@ -2255,14 +2360,14 @@ var TokenService = class TokenService2 {
2255
2360
  verifyRefreshToken(token) {
2256
2361
  let decoded;
2257
2362
  try {
2258
- decoded = jwt.verify(token, this.config.jwt.refreshSecret);
2363
+ decoded = jwt.verify(token, this.config.jwt.refreshSecret, { algorithms: ["HS256"] });
2259
2364
  } catch {
2260
2365
  Err7(this.t("errors.tokenVerificationFailed"), 401);
2261
2366
  }
2262
- if (decoded.type && decoded.type !== "refresh") {
2367
+ if (!decoded || typeof decoded !== "object" || decoded.type !== "refresh" || typeof decoded.userId !== "string" || !decoded.userId || !Number.isFinite(decoded.exp)) {
2263
2368
  Err7(this.t("errors.tokenVerificationFailed"), 401);
2264
2369
  }
2265
- if (!decoded.tokenFamily) {
2370
+ if (typeof decoded.tokenFamily !== "string" || !decoded.tokenFamily) {
2266
2371
  Err7(this.t("errors.tokenVerificationFailed"), 401);
2267
2372
  }
2268
2373
  return { userId: decoded.userId, tokenFamily: decoded.tokenFamily };
@@ -2329,8 +2434,9 @@ var TokenService = class TokenService2 {
2329
2434
  */
2330
2435
  async recoverSessionFromCookie() {
2331
2436
  const { userId, tokenFamily } = await this.resolveRefreshSessionFromCookie();
2332
- const user = await this.requireActiveRefreshUser(userId, tokenFamily);
2333
2437
  const sessionVersion = await this.getUserSessionVersion(userId);
2438
+ const user = await this.requireActiveRefreshUser(userId, tokenFamily);
2439
+ await this.assertSessionVersion(userId, sessionVersion);
2334
2440
  return {
2335
2441
  user,
2336
2442
  roles: user.role ? [user.role] : [],
@@ -2367,16 +2473,18 @@ var TokenService = class TokenService2 {
2367
2473
  return user;
2368
2474
  }
2369
2475
  async getUserById(userId) {
2370
- const cacheKey = `auth:user:${userId}`;
2476
+ const version = await this.getUserSessionVersion(userId);
2477
+ const cacheKey = `auth:user:${userId}:${version}`;
2371
2478
  if (typeof this.cache.getOrSet === "function") {
2372
- return this.cache.getOrSet(cacheKey, () => this.tokenRepository.getUser(userId), 3e4);
2479
+ const user2 = await this.cache.getOrSet(cacheKey, () => this.tokenRepository.getUser(userId), 3e4);
2480
+ await this.assertSessionVersion(userId, version);
2481
+ return user2;
2373
2482
  }
2374
2483
  const cached = await this.cache.get(cacheKey);
2375
- if (cached)
2376
- return JSON.parse(cached);
2377
- const user = await this.tokenRepository.getUser(userId);
2378
- if (user)
2484
+ const user = cached ? JSON.parse(cached) : await this.tokenRepository.getUser(userId);
2485
+ if (!cached && user)
2379
2486
  await this.cache.set(cacheKey, JSON.stringify(user), 3e4);
2487
+ await this.assertSessionVersion(userId, version);
2380
2488
  return user;
2381
2489
  }
2382
2490
  // ============ TOKEN GENERATION ============
@@ -2389,9 +2497,9 @@ var TokenService = class TokenService2 {
2389
2497
  decodeAccessToken(token) {
2390
2498
  return jwt.decode(token);
2391
2499
  }
2392
- async signAccessToken(data) {
2500
+ async signAccessToken(data, capturedVersion) {
2393
2501
  const jti = nanoid4(16);
2394
- const sessionVersion = await this.getUserSessionVersion(data.userId);
2502
+ const sessionVersion = capturedVersion ?? await this.getUserSessionVersion(data.userId);
2395
2503
  const expiresAt = this.expiresAt(this.config.jwt.accessExpiresIn);
2396
2504
  if (sessionVersion > 0) {
2397
2505
  await this.invalidation.touchSessionVersion(data.userId);
@@ -2446,7 +2554,7 @@ var TokenService = class TokenService2 {
2446
2554
  * rebuild the token its rotation issued and hand the browser that one.
2447
2555
  */
2448
2556
  successorRefreshToken(replaced, data, expiresAt) {
2449
- const jti = createHmac("sha256", this.config.jwt.refreshSecret).update(`successor:${replaced}`).digest("base64url").slice(0, 16);
2557
+ const jti = createHmac2("sha256", this.config.jwt.refreshSecret).update(`successor:${replaced}`).digest("base64url").slice(0, 16);
2450
2558
  const iat = expiresAt - Math.floor(timestring3(this.config.jwt.refreshExpiresIn, "s"));
2451
2559
  const token = jwt.sign({ ...data, jti, type: "refresh", iat, exp: expiresAt }, this.config.jwt.refreshSecret);
2452
2560
  return { token, expiresAt };
@@ -2454,7 +2562,8 @@ var TokenService = class TokenService2 {
2454
2562
  generateRefreshToken(data) {
2455
2563
  return this.signRefreshToken({ userId: data.userId, tokenFamily: data.tokenFamily ?? nanoid4(16) }).token;
2456
2564
  }
2457
- async createTokenPair(userId, family, replaced) {
2565
+ async createTokenPair(userId, family, replaced, capturedVersion) {
2566
+ const sessionVersion = capturedVersion ?? await this.getUserSessionVersion(userId);
2458
2567
  const { roleName, permissions } = await this.tokenRepository.getRoleAndPermissions(userId);
2459
2568
  const accessTokenData = {
2460
2569
  userId,
@@ -2462,7 +2571,8 @@ var TokenService = class TokenService2 {
2462
2571
  permissions: permissions ?? [],
2463
2572
  tokenFamily: family
2464
2573
  };
2465
- const access = await this.signAccessToken(accessTokenData);
2574
+ await this.assertSessionVersion(userId, sessionVersion);
2575
+ const access = await this.signAccessToken(accessTokenData, sessionVersion);
2466
2576
  const refresh = replaced ? this.successorRefreshToken(replaced, { userId, tokenFamily: family }, this.expiresAt(this.config.jwt.refreshExpiresIn)) : this.signRefreshToken({ userId, tokenFamily: family });
2467
2577
  return {
2468
2578
  userId,
@@ -2476,12 +2586,20 @@ var TokenService = class TokenService2 {
2476
2586
  refreshTokenExpiresAt: refresh.expiresAt
2477
2587
  };
2478
2588
  }
2479
- async generateTokens(userId, tokenFamily) {
2589
+ async generateTokens(userId, tokenFamily, capturedVersion) {
2480
2590
  const family = tokenFamily ?? nanoid4(16);
2481
- const generated = await this.createTokenPair(userId, family);
2591
+ const generated = await this.createTokenPair(userId, family, void 0, capturedVersion);
2482
2592
  await this.storeRefreshToken(userId, generated.refreshToken, family);
2593
+ await this.assertSessionVersion(userId, generated.sessionVersion, family);
2483
2594
  return generated;
2484
2595
  }
2596
+ async assertSessionVersion(userId, expected, issuedFamily) {
2597
+ if (await this.getUserSessionVersion(userId) !== expected) {
2598
+ if (issuedFamily)
2599
+ await this.revokeFamily(issuedFamily);
2600
+ Err7(this.t("errors.tokenRevoked"), 401);
2601
+ }
2602
+ }
2485
2603
  // ============ TOKEN BLACKLIST (Cache) ============
2486
2604
  /**
2487
2605
  * Blacklist an access token by its jti
@@ -2565,6 +2683,7 @@ var TokenService = class TokenService2 {
2565
2683
  if (!await this.invalidation.markFamilyIssued(tokenFamily, userId)) {
2566
2684
  this.rejectRefreshSession();
2567
2685
  }
2686
+ await this.assertSessionVersion(userId, generated.sessionVersion);
2568
2687
  return generated;
2569
2688
  }
2570
2689
  /**
@@ -2619,12 +2738,15 @@ var TokenService = class TokenService2 {
2619
2738
  async serveReplacedToken(userId, tokenFamily, replaced, stored) {
2620
2739
  const successor = this.successorRefreshToken(replaced, { userId, tokenFamily }, Math.floor(storedTimeMs(stored.expiresAt) / 1e3));
2621
2740
  const refresh = this.hashToken(successor.token) === stored.token ? successor : void 0;
2741
+ const sessionVersion = await this.getUserSessionVersion(userId);
2622
2742
  const { roleName, permissions } = await this.tokenRepository.getRoleAndPermissions(userId);
2623
2743
  const roles = roleName ? [roleName] : [];
2624
- const access = await this.signAccessToken({ userId, roles, permissions: permissions ?? [], tokenFamily });
2744
+ await this.assertSessionVersion(userId, sessionVersion);
2745
+ const access = await this.signAccessToken({ userId, roles, permissions: permissions ?? [], tokenFamily }, sessionVersion);
2625
2746
  if (!await this.invalidation.markFamilyIssued(tokenFamily, userId)) {
2626
2747
  this.rejectRefreshSession();
2627
2748
  }
2749
+ await this.assertSessionVersion(userId, sessionVersion);
2628
2750
  return {
2629
2751
  userId,
2630
2752
  tokenFamily,
@@ -2652,11 +2774,7 @@ var TokenService = class TokenService2 {
2652
2774
  }
2653
2775
  /** Revoke every refresh session for a user (password change/reset, logout-all). */
2654
2776
  async revokeAllForUser(userId) {
2655
- const revoked = await this.tokenRepository.revokeAllForUser(userId);
2656
- if (Array.isArray(revoked)) {
2657
- await Promise.all(revoked.map((row) => row?.tokenFamily).filter((family) => typeof family === "string" && !!family).map((family) => this.invalidation.markFamilyRevoked(family)));
2658
- }
2659
- return revoked;
2777
+ return this.invalidation.revokeAllForUser(userId);
2660
2778
  }
2661
2779
  /** Revoke a single refresh session (one family). */
2662
2780
  async revokeFamily(tokenFamily) {
@@ -2771,34 +2889,45 @@ var TokenService = class TokenService2 {
2771
2889
  * account invites). Stores the jti in cache so the token can only be used
2772
2890
  * once, with a TTL matching the JWT expiry.
2773
2891
  */
2774
- async generateSetPasswordToken(userId, type, expiresIn) {
2892
+ async generateSetPasswordToken(userId, type, expiresIn, expectedEmail) {
2893
+ const user = await this.tokenRepository.getCredentialState(userId);
2894
+ if (!user || expectedEmail !== void 0 && user.email.trim().toLowerCase() !== expectedEmail.trim().toLowerCase()) {
2895
+ Err7(this.t("errors.invalidResetToken"), 401);
2896
+ }
2897
+ const credentialState = credentialFingerprint(this.config.jwt.refreshSecret, user);
2775
2898
  const jti = nanoid4(16);
2776
2899
  const data = {
2777
2900
  userId,
2778
2901
  type,
2779
2902
  jti,
2780
- timestamp: Date.now()
2903
+ timestamp: Date.now(),
2904
+ credentialState
2781
2905
  };
2782
2906
  const token = jwt.sign(data, this.config.jwt.refreshSecret, {
2783
2907
  expiresIn
2784
2908
  });
2785
2909
  await this.cache.set(`${this.resetTokenPrefix}${userId}`, jti, timestring3(expiresIn, "ms"));
2910
+ const current = await this.tokenRepository.getCredentialState(userId);
2911
+ if (!current || credentialFingerprint(this.config.jwt.refreshSecret, current) !== credentialState) {
2912
+ await this.discardSetPasswordToken(userId, jti);
2913
+ Err7(this.t("errors.invalidResetToken"), 401);
2914
+ }
2786
2915
  return { token, userId, jti };
2787
2916
  }
2788
2917
  /**
2789
2918
  * Generate secure password reset token
2790
2919
  * Returns both the plain token (to send via email) and userId for identification
2791
2920
  */
2792
- async generateResetToken(userId) {
2793
- return this.generateSetPasswordToken(userId, "reset", "1h");
2921
+ async generateResetToken(userId, expectedEmail) {
2922
+ return this.generateSetPasswordToken(userId, "reset", "1h", expectedEmail);
2794
2923
  }
2795
2924
  /**
2796
2925
  * Generate secure account-invite token.
2797
2926
  * Longer expiry (3d) than reset because an invited user may not check
2798
2927
  * their email immediately. Consumed via the same reset-password endpoint.
2799
2928
  */
2800
- async generateInviteToken(userId) {
2801
- return this.generateSetPasswordToken(userId, "invite", "3d");
2929
+ async generateInviteToken(userId, expectedEmail) {
2930
+ return this.generateSetPasswordToken(userId, "invite", "3d", expectedEmail);
2802
2931
  }
2803
2932
  /**
2804
2933
  * Discard a set-password token this process just minted, identified by the
@@ -2836,11 +2965,11 @@ var TokenService = class TokenService2 {
2836
2965
  async consumeSetPasswordToken(token) {
2837
2966
  let decoded;
2838
2967
  try {
2839
- decoded = jwt.verify(token, this.config.jwt.refreshSecret);
2968
+ decoded = jwt.verify(token, this.config.jwt.refreshSecret, { algorithms: ["HS256"] });
2840
2969
  } catch {
2841
2970
  Err7(this.t("errors.resetTokenExpired"));
2842
2971
  }
2843
- if (decoded.type !== "reset" && decoded.type !== "invite" || !decoded.jti) {
2972
+ if (!decoded || typeof decoded !== "object" || decoded.type !== "reset" && decoded.type !== "invite" || typeof decoded.userId !== "string" || !decoded.userId || typeof decoded.jti !== "string" || !decoded.jti || typeof decoded.credentialState !== "string" || !decoded.credentialState || !Number.isFinite(decoded.exp)) {
2844
2973
  Err7(this.t("errors.invalidResetToken"));
2845
2974
  }
2846
2975
  const consume = this.cache.compareAndDelete;
@@ -2848,12 +2977,21 @@ var TokenService = class TokenService2 {
2848
2977
  Err7.invalidOperation("Password reset requires a cache with atomic compare-and-delete");
2849
2978
  }
2850
2979
  const key = `${this.resetTokenPrefix}${decoded.userId}`;
2980
+ const matches = /* @__PURE__ */ __name(async () => {
2981
+ const current = await this.tokenRepository.getCredentialState(decoded.userId);
2982
+ return current && credentialFingerprint(this.config.jwt.refreshSecret, current) === decoded.credentialState;
2983
+ }, "matches");
2984
+ if (!await matches())
2985
+ Err7(this.t("errors.invalidResetToken"));
2851
2986
  if (!await consume.call(this.cache, key, decoded.jti)) {
2852
2987
  Err7(this.t("errors.invalidResetToken"));
2853
2988
  }
2989
+ if (!await matches())
2990
+ Err7(this.t("errors.invalidResetToken"));
2854
2991
  return {
2855
2992
  userId: decoded.userId,
2856
- type: decoded.type
2993
+ type: decoded.type,
2994
+ credentialState: decoded.credentialState
2857
2995
  };
2858
2996
  }
2859
2997
  /**
@@ -2878,7 +3016,7 @@ __decorate12([
2878
3016
  ], TokenService.prototype, "t", void 0);
2879
3017
  TokenService = TokenService_1 = __decorate12([
2880
3018
  Injectable7(),
2881
- __metadata12("design:paramtypes", [typeof (_a7 = typeof TokenRepository !== "undefined" && TokenRepository) === "function" ? _a7 : Object, typeof (_b5 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _b5 : Object, typeof (_c2 = typeof CacheService2 !== "undefined" && CacheService2) === "function" ? _c2 : Object, typeof (_d2 = typeof CredentialSetupRequirementRepository !== "undefined" && CredentialSetupRequirementRepository) === "function" ? _d2 : Object, typeof (_e2 = typeof SessionInvalidationService !== "undefined" && SessionInvalidationService) === "function" ? _e2 : Object])
3019
+ __metadata12("design:paramtypes", [typeof (_a7 = typeof TokenRepository !== "undefined" && TokenRepository) === "function" ? _a7 : Object, typeof (_b5 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _b5 : Object, typeof (_c4 = typeof CacheService2 !== "undefined" && CacheService2) === "function" ? _c4 : Object, typeof (_d3 = typeof CredentialSetupRequirementRepository !== "undefined" && CredentialSetupRequirementRepository) === "function" ? _d3 : Object, typeof (_e2 = typeof SessionInvalidationService !== "undefined" && SessionInvalidationService) === "function" ? _e2 : Object])
2882
3020
  ], TokenService);
2883
3021
 
2884
3022
  // src/auth/AuthService.ts
@@ -2972,7 +3110,7 @@ var __metadata13 = function(k, v) {
2972
3110
  };
2973
3111
  var _a8;
2974
3112
  var _b6;
2975
- var _c3;
3113
+ var _c5;
2976
3114
  var CredentialSetupRequirementService = class CredentialSetupRequirementService2 {
2977
3115
  static {
2978
3116
  __name(this, "CredentialSetupRequirementService");
@@ -3015,7 +3153,7 @@ __decorate13([
3015
3153
  Transaction2({ retries: 2 }),
3016
3154
  __metadata13("design:type", Function),
3017
3155
  __metadata13("design:paramtypes", [String, String, Object]),
3018
- __metadata13("design:returntype", typeof (_c3 = typeof Promise !== "undefined" && Promise) === "function" ? _c3 : Object)
3156
+ __metadata13("design:returntype", typeof (_c5 = typeof Promise !== "undefined" && Promise) === "function" ? _c5 : Object)
3019
3157
  ], CredentialSetupRequirementService.prototype, "markRequired", null);
3020
3158
  CredentialSetupRequirementService = __decorate13([
3021
3159
  Injectable8(),
@@ -3044,6 +3182,9 @@ var credentialSetupError = /* @__PURE__ */ __name((code, message, status = 400)
3044
3182
  throw new BaseError(code, message, status);
3045
3183
  }, "credentialSetupError");
3046
3184
 
3185
+ // src/auth/verifiedCredential.ts
3186
+ var verifiedCredentials = /* @__PURE__ */ new WeakMap();
3187
+
3047
3188
  // src/auth/AuthSessionService.ts
3048
3189
  var __decorate14 = function(decorators, target, key, desc) {
3049
3190
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -3056,8 +3197,8 @@ var __metadata14 = function(k, v) {
3056
3197
  };
3057
3198
  var _a9;
3058
3199
  var _b7;
3059
- var _c4;
3060
- var _d3;
3200
+ var _c6;
3201
+ var _d4;
3061
3202
  var AuthSessionService = class AuthSessionService2 {
3062
3203
  static {
3063
3204
  __name(this, "AuthSessionService");
@@ -3073,17 +3214,28 @@ var AuthSessionService = class AuthSessionService2 {
3073
3214
  this.credentialSetupRequirements = credentialSetupRequirements;
3074
3215
  }
3075
3216
  async establish(user) {
3076
- if (user.status !== "active") {
3217
+ const sessionVersion = await this.tokenService.getSessionVersion(user.id);
3218
+ const current = await this.userService.getAuthRecordById(user.id);
3219
+ if (!current || current.status !== "active" || user.status !== "active") {
3077
3220
  Err9("oauth_account_inactive", 403);
3078
3221
  }
3222
+ const verifiedHash = verifiedCredentials.get(user);
3223
+ if (verifiedHash !== void 0 && verifiedHash !== current.password || ["email", "phone", "emailVerified", "roleId"].some((field) => field in user && (user[field] ?? null) !== (current[field] ?? null))) {
3224
+ Err9("errors.invalidCredentials", 401);
3225
+ }
3079
3226
  if (await this.credentialSetupRequirements?.isRequired(user.id, PASSWORD_SETUP_PURPOSE)) {
3080
3227
  credentialSetupError(CREDENTIAL_SETUP_CODES.REQUIRED, "Credential setup is required before a session can be established", 403);
3081
3228
  }
3082
3229
  await this.tokenService.deleteExpiredSessions();
3083
- const generated = await this.tokenService.generateTokens(user.id);
3084
- this.cookieManager.setRefreshToken(generated.refreshToken);
3230
+ const generated = await this.tokenService.generateTokens(user.id, void 0, sessionVersion);
3085
3231
  await this.userService.updateLastLogin(user.id);
3086
- const { roles, permissions, sessionVersion, tokenFamily } = generated;
3232
+ if (await this.tokenService.getSessionVersion(user.id) !== generated.sessionVersion) {
3233
+ await this.tokenService.revokeFamily(generated.tokenFamily);
3234
+ Err9("errors.tokenRevoked", 401);
3235
+ }
3236
+ this.cookieManager.setRefreshToken(generated.refreshToken);
3237
+ const { roles, permissions, tokenFamily } = generated;
3238
+ user = { ...user, role: roles[0] ?? null };
3087
3239
  this.cookieManager.setSessionCookie({
3088
3240
  user: {
3089
3241
  id: user.id,
@@ -3103,22 +3255,22 @@ var AuthSessionService = class AuthSessionService2 {
3103
3255
  };
3104
3256
  AuthSessionService = __decorate14([
3105
3257
  Injectable9(),
3106
- __metadata14("design:paramtypes", [typeof (_a9 = typeof TokenService !== "undefined" && TokenService) === "function" ? _a9 : Object, typeof (_b7 = typeof UserService !== "undefined" && UserService) === "function" ? _b7 : Object, typeof (_c4 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _c4 : Object, typeof (_d3 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _d3 : Object])
3258
+ __metadata14("design:paramtypes", [typeof (_a9 = typeof TokenService !== "undefined" && TokenService) === "function" ? _a9 : Object, typeof (_b7 = typeof UserService !== "undefined" && UserService) === "function" ? _b7 : Object, typeof (_c6 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _c6 : Object, typeof (_d4 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _d4 : Object])
3107
3259
  ], AuthSessionService);
3108
3260
 
3109
3261
  // src/credentialSetup/PasswordSetupService.ts
3110
- import { Inject as Inject11, Injectable as Injectable11 } from "najm-core";
3262
+ import { Inject as Inject12, Injectable as Injectable11 } from "najm-core";
3111
3263
  import { I18n as I18n7 } from "najm-i18n";
3112
3264
 
3113
3265
  // src/credentialSetup/CredentialSetupService.ts
3114
3266
  import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
3115
3267
  import { CookieService as CookieService2 } from "najm-cookies";
3116
- import { Err as Err11, Injectable as Injectable10 } from "najm-core";
3268
+ import { Err as Err11, Inject as Inject11, Injectable as Injectable10 } from "najm-core";
3117
3269
  import { Transaction as Transaction3 } from "najm-database";
3118
3270
  import { I18n as I18n6 } from "najm-i18n";
3119
3271
 
3120
3272
  // src/credentialSetup/CredentialSetupRepository.ts
3121
- import { and as and3, eq as eq6, gt, isNull as isNull2, lt as lt2 } from "drizzle-orm";
3273
+ import { and as and4, eq as eq6, gt, isNull as isNull3, lt as lt2 } from "drizzle-orm";
3122
3274
  import { Err as Err10, Inject as Inject10, Repository as Repository5 } from "najm-core";
3123
3275
  import { DB as DB5 } from "najm-database";
3124
3276
  var __decorate15 = function(decorators, target, key, desc) {
@@ -3145,7 +3297,7 @@ var CredentialSetupRepository = class CredentialSetupRepository2 {
3145
3297
  }
3146
3298
  async replaceActive(data) {
3147
3299
  const now = (/* @__PURE__ */ new Date()).toISOString();
3148
- await this.db.update(this.sessions).set({ revokedAt: now, updatedAt: now }).where(and3(eq6(this.sessions.userId, data.userId), eq6(this.sessions.purpose, data.purpose), isNull2(this.sessions.consumedAt), isNull2(this.sessions.revokedAt)));
3300
+ await this.db.update(this.sessions).set({ revokedAt: now, updatedAt: now }).where(and4(eq6(this.sessions.userId, data.userId), eq6(this.sessions.purpose, data.purpose), isNull3(this.sessions.consumedAt), isNull3(this.sessions.revokedAt)));
3149
3301
  const [session] = await this.db.insert(this.sessions).values(data).returning({
3150
3302
  userId: this.sessions.userId,
3151
3303
  purpose: this.sessions.purpose,
@@ -3158,12 +3310,12 @@ var CredentialSetupRepository = class CredentialSetupRepository2 {
3158
3310
  userId: this.sessions.userId,
3159
3311
  purpose: this.sessions.purpose,
3160
3312
  expiresAt: this.sessions.expiresAt
3161
- }).from(this.sessions).where(and3(eq6(this.sessions.tokenHash, tokenHash), eq6(this.sessions.purpose, purpose), isNull2(this.sessions.consumedAt), isNull2(this.sessions.revokedAt), gt(this.sessions.expiresAt, (/* @__PURE__ */ new Date()).toISOString()))).limit(1);
3313
+ }).from(this.sessions).where(and4(eq6(this.sessions.tokenHash, tokenHash), eq6(this.sessions.purpose, purpose), isNull3(this.sessions.consumedAt), isNull3(this.sessions.revokedAt), gt(this.sessions.expiresAt, (/* @__PURE__ */ new Date()).toISOString()))).limit(1);
3162
3314
  return session;
3163
3315
  }
3164
3316
  async consume(tokenHash, purpose) {
3165
3317
  const now = (/* @__PURE__ */ new Date()).toISOString();
3166
- const [session] = await this.db.update(this.sessions).set({ consumedAt: now, updatedAt: now }).where(and3(eq6(this.sessions.tokenHash, tokenHash), eq6(this.sessions.purpose, purpose), isNull2(this.sessions.consumedAt), isNull2(this.sessions.revokedAt), gt(this.sessions.expiresAt, now))).returning({
3318
+ const [session] = await this.db.update(this.sessions).set({ consumedAt: now, updatedAt: now }).where(and4(eq6(this.sessions.tokenHash, tokenHash), eq6(this.sessions.purpose, purpose), isNull3(this.sessions.consumedAt), isNull3(this.sessions.revokedAt), gt(this.sessions.expiresAt, now))).returning({
3167
3319
  userId: this.sessions.userId,
3168
3320
  purpose: this.sessions.purpose,
3169
3321
  expiresAt: this.sessions.expiresAt
@@ -3172,7 +3324,7 @@ var CredentialSetupRepository = class CredentialSetupRepository2 {
3172
3324
  }
3173
3325
  async revoke(tokenHash, purpose) {
3174
3326
  const now = (/* @__PURE__ */ new Date()).toISOString();
3175
- const [session] = await this.db.update(this.sessions).set({ revokedAt: now, updatedAt: now }).where(and3(eq6(this.sessions.tokenHash, tokenHash), eq6(this.sessions.purpose, purpose), isNull2(this.sessions.consumedAt), isNull2(this.sessions.revokedAt))).returning({ userId: this.sessions.userId });
3327
+ const [session] = await this.db.update(this.sessions).set({ revokedAt: now, updatedAt: now }).where(and4(eq6(this.sessions.tokenHash, tokenHash), eq6(this.sessions.purpose, purpose), isNull3(this.sessions.consumedAt), isNull3(this.sessions.revokedAt))).returning({ userId: this.sessions.userId });
3176
3328
  return session;
3177
3329
  }
3178
3330
  async deleteExpired() {
@@ -3203,11 +3355,12 @@ var __metadata16 = function(k, v) {
3203
3355
  };
3204
3356
  var _a10;
3205
3357
  var _b8;
3206
- var _c5;
3207
- var _d4;
3358
+ var _c7;
3359
+ var _d5;
3208
3360
  var _e3;
3209
3361
  var _f2;
3210
3362
  var _g2;
3363
+ var _h2;
3211
3364
  var DEFAULT_CREDENTIAL_SETUP_COOKIE_NAME = "najm.credential-setup";
3212
3365
  var DEFAULT_CREDENTIAL_SETUP_TTL_MS = 10 * 60 * 1e3;
3213
3366
  var MAX_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -3220,12 +3373,15 @@ var CredentialSetupService = class CredentialSetupService2 {
3220
3373
  tokens;
3221
3374
  authCookies;
3222
3375
  cookies;
3376
+ users;
3223
3377
  t;
3224
- constructor(repository, tokens, authCookies, cookies2) {
3378
+ config;
3379
+ constructor(repository, tokens, authCookies, cookies2, users) {
3225
3380
  this.repository = repository;
3226
3381
  this.tokens = tokens;
3227
3382
  this.authCookies = authCookies;
3228
3383
  this.cookies = cookies2;
3384
+ this.users = users;
3229
3385
  }
3230
3386
  /**
3231
3387
  * Revoke normal auth sessions and issue a single-purpose, short-lived,
@@ -3233,7 +3389,10 @@ var CredentialSetupService = class CredentialSetupService2 {
3233
3389
  */
3234
3390
  async begin(userId, options) {
3235
3391
  const resolved = this.resolveOptions(options);
3236
- const token = randomBytes2(32).toString("base64url");
3392
+ const user = await this.users.getRawById(userId);
3393
+ if (!user)
3394
+ this.sessionInvalid();
3395
+ const token = `${randomBytes2(32).toString("base64url")}.${credentialFingerprint(this.config.jwt.refreshSecret, user)}`;
3237
3396
  const expiresAt = new Date(Date.now() + resolved.ttlMs).toISOString();
3238
3397
  await this.repository.deleteExpired();
3239
3398
  await this.repository.replaceActive({
@@ -3244,6 +3403,7 @@ var CredentialSetupService = class CredentialSetupService2 {
3244
3403
  });
3245
3404
  await this.tokens.invalidateUserAccessTokens(userId);
3246
3405
  await this.tokens.revokeAllForUser(userId);
3406
+ await this.requireCurrentCredential(userId, token, resolved);
3247
3407
  this.authCookies.clearRefreshToken();
3248
3408
  this.authCookies.clearSessionCookie();
3249
3409
  this.setCookie(token, resolved);
@@ -3260,6 +3420,7 @@ var CredentialSetupService = class CredentialSetupService2 {
3260
3420
  this.clearCookie(resolved);
3261
3421
  this.sessionInvalid();
3262
3422
  }
3423
+ await this.requireCurrentCredential(session.userId, token, resolved);
3263
3424
  return session;
3264
3425
  }
3265
3426
  /**
@@ -3277,10 +3438,19 @@ var CredentialSetupService = class CredentialSetupService2 {
3277
3438
  this.clearCookie(resolved);
3278
3439
  this.sessionInvalid();
3279
3440
  }
3441
+ await this.requireCurrentCredential(session.userId, token, resolved);
3280
3442
  const result = await complete(session);
3281
3443
  this.clearCookie(resolved);
3282
3444
  return result;
3283
3445
  }
3446
+ async requireCurrentCredential(userId, token, options) {
3447
+ const fingerprint = token.split(".")[1];
3448
+ const user = await this.users.getRawById(userId);
3449
+ if (!fingerprint || !user || credentialFingerprint(this.config.jwt.refreshSecret, user) !== fingerprint) {
3450
+ this.clearCookie(options);
3451
+ this.sessionInvalid();
3452
+ }
3453
+ }
3284
3454
  async cancel(options) {
3285
3455
  const resolved = this.resolveOptions(options);
3286
3456
  const token = this.cookies.get(resolved.cookieName);
@@ -3345,27 +3515,31 @@ __decorate16([
3345
3515
  I18n6("auth"),
3346
3516
  __metadata16("design:type", Object)
3347
3517
  ], CredentialSetupService.prototype, "t", void 0);
3518
+ __decorate16([
3519
+ Inject11(AUTH_CONFIG),
3520
+ __metadata16("design:type", Object)
3521
+ ], CredentialSetupService.prototype, "config", void 0);
3348
3522
  __decorate16([
3349
3523
  Transaction3({ retries: 2 }),
3350
3524
  __metadata16("design:type", Function),
3351
3525
  __metadata16("design:paramtypes", [String, Object]),
3352
- __metadata16("design:returntype", typeof (_e3 = typeof Promise !== "undefined" && Promise) === "function" ? _e3 : Object)
3526
+ __metadata16("design:returntype", typeof (_f2 = typeof Promise !== "undefined" && Promise) === "function" ? _f2 : Object)
3353
3527
  ], CredentialSetupService.prototype, "begin", null);
3354
3528
  __decorate16([
3355
3529
  Transaction3({ retries: 2 }),
3356
3530
  __metadata16("design:type", Function),
3357
3531
  __metadata16("design:paramtypes", [Object, Function]),
3358
- __metadata16("design:returntype", typeof (_f2 = typeof Promise !== "undefined" && Promise) === "function" ? _f2 : Object)
3532
+ __metadata16("design:returntype", typeof (_g2 = typeof Promise !== "undefined" && Promise) === "function" ? _g2 : Object)
3359
3533
  ], CredentialSetupService.prototype, "consume", null);
3360
3534
  __decorate16([
3361
3535
  Transaction3({ retries: 2 }),
3362
3536
  __metadata16("design:type", Function),
3363
3537
  __metadata16("design:paramtypes", [Object]),
3364
- __metadata16("design:returntype", typeof (_g2 = typeof Promise !== "undefined" && Promise) === "function" ? _g2 : Object)
3538
+ __metadata16("design:returntype", typeof (_h2 = typeof Promise !== "undefined" && Promise) === "function" ? _h2 : Object)
3365
3539
  ], CredentialSetupService.prototype, "cancel", null);
3366
3540
  CredentialSetupService = __decorate16([
3367
3541
  Injectable10(),
3368
- __metadata16("design:paramtypes", [typeof (_a10 = typeof CredentialSetupRepository !== "undefined" && CredentialSetupRepository) === "function" ? _a10 : Object, typeof (_b8 = typeof TokenService !== "undefined" && TokenService) === "function" ? _b8 : Object, typeof (_c5 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _c5 : Object, typeof (_d4 = typeof CookieService2 !== "undefined" && CookieService2) === "function" ? _d4 : Object])
3542
+ __metadata16("design:paramtypes", [typeof (_a10 = typeof CredentialSetupRepository !== "undefined" && CredentialSetupRepository) === "function" ? _a10 : Object, typeof (_b8 = typeof TokenService !== "undefined" && TokenService) === "function" ? _b8 : Object, typeof (_c7 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _c7 : Object, typeof (_d5 = typeof CookieService2 !== "undefined" && CookieService2) === "function" ? _d5 : Object, typeof (_e3 = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _e3 : Object])
3369
3543
  ], CredentialSetupService);
3370
3544
 
3371
3545
  // src/credentialSetup/PasswordSetupService.ts
@@ -3380,8 +3554,8 @@ var __metadata17 = function(k, v) {
3380
3554
  };
3381
3555
  var _a11;
3382
3556
  var _b9;
3383
- var _c6;
3384
- var _d5;
3557
+ var _c8;
3558
+ var _d6;
3385
3559
  var _e4;
3386
3560
  var _f3;
3387
3561
  var PasswordSetupService = class PasswordSetupService2 {
@@ -3440,9 +3614,9 @@ var PasswordSetupService = class PasswordSetupService2 {
3440
3614
  */
3441
3615
  async change(newPassword) {
3442
3616
  const { userId } = await this.setup.require(this.options);
3443
- const password = await this.validateReplacement(userId, newPassword);
3617
+ const { password, credential } = await this.validateReplacement(userId, newPassword);
3444
3618
  const hashed = await this.encryption.hashPassword(password);
3445
- await this.setup.consume(this.options, (session) => this.persist(session.userId, hashed));
3619
+ await this.setup.consume(this.options, (session) => this.persist(session.userId, hashed, credential));
3446
3620
  return { changed: true, signInAgain: true };
3447
3621
  }
3448
3622
  async cancel() {
@@ -3465,15 +3639,18 @@ var PasswordSetupService = class PasswordSetupService2 {
3465
3639
  if (await this.isCurrentPassword(newPassword, kind.normalize(newPassword), user.password)) {
3466
3640
  credentialSetupError(CREDENTIAL_SETUP_CODES.SAME_PASSWORD, this.message("errors.credentialSetupSamePassword", "Choose a password different from your current one."), 400);
3467
3641
  }
3468
- return newPassword;
3642
+ return { password: newPassword, credential: user };
3469
3643
  }
3470
3644
  /**
3471
3645
  * Runs inside the setup-session consumption transaction, so the password
3472
3646
  * update, the requirement completion, and the one-time session all commit or
3473
3647
  * roll back together.
3474
3648
  */
3475
- async persist(userId, password) {
3476
- await this.userRecords.update(userId, { password });
3649
+ async persist(userId, password, credential) {
3650
+ if (userId !== credential.id)
3651
+ this.validator.checkCredentialUnchanged(void 0);
3652
+ const updated = await this.userRecords.updateWithCredential(userId, { password }, credential);
3653
+ this.validator.checkCredentialUnchanged(updated);
3477
3654
  const completed = await this.requirements.completeRequirement(userId, PASSWORD_SETUP_PURPOSE);
3478
3655
  if (!completed) {
3479
3656
  credentialSetupError(CREDENTIAL_SETUP_CODES.ALREADY_COMPLETED, this.message("errors.credentialSetupAlreadyCompleted", "The password was already replaced. Please sign in again."), 409);
@@ -3485,6 +3662,9 @@ var PasswordSetupService = class PasswordSetupService2 {
3485
3662
  if (!parsed.success) {
3486
3663
  credentialSetupError(CREDENTIAL_SETUP_CODES.PASSWORD_REJECTED, parsed.error.issues[0]?.message ?? this.message("errors.credentialSetupPasswordRejected", "Choose a different password."), 422);
3487
3664
  }
3665
+ if (!parsed.data || new TextEncoder().encode(parsed.data).length > 72) {
3666
+ credentialSetupError(CREDENTIAL_SETUP_CODES.PASSWORD_REJECTED, "Password must be between 1 and 72 bytes", 422);
3667
+ }
3488
3668
  return parsed.data;
3489
3669
  }
3490
3670
  /**
@@ -3500,7 +3680,7 @@ var PasswordSetupService = class PasswordSetupService2 {
3500
3680
  }
3501
3681
  };
3502
3682
  __decorate17([
3503
- Inject11(AUTH_CONFIG),
3683
+ Inject12(AUTH_CONFIG),
3504
3684
  __metadata17("design:type", Object)
3505
3685
  ], PasswordSetupService.prototype, "config", void 0);
3506
3686
  __decorate17([
@@ -3509,7 +3689,7 @@ __decorate17([
3509
3689
  ], PasswordSetupService.prototype, "t", void 0);
3510
3690
  PasswordSetupService = __decorate17([
3511
3691
  Injectable11(),
3512
- __metadata17("design:paramtypes", [typeof (_a11 = typeof CredentialSetupService !== "undefined" && CredentialSetupService) === "function" ? _a11 : Object, typeof (_b9 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _b9 : Object, typeof (_c6 = typeof UserService !== "undefined" && UserService) === "function" ? _c6 : Object, typeof (_d5 = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _d5 : Object, typeof (_e4 = typeof UserValidator !== "undefined" && UserValidator) === "function" ? _e4 : Object, typeof (_f3 = typeof EncryptionService !== "undefined" && EncryptionService) === "function" ? _f3 : Object])
3692
+ __metadata17("design:paramtypes", [typeof (_a11 = typeof CredentialSetupService !== "undefined" && CredentialSetupService) === "function" ? _a11 : Object, typeof (_b9 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _b9 : Object, typeof (_c8 = typeof UserService !== "undefined" && UserService) === "function" ? _c8 : Object, typeof (_d6 = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _d6 : Object, typeof (_e4 = typeof UserValidator !== "undefined" && UserValidator) === "function" ? _e4 : Object, typeof (_f3 = typeof EncryptionService !== "undefined" && EncryptionService) === "function" ? _f3 : Object])
3513
3693
  ], PasswordSetupService);
3514
3694
 
3515
3695
  // src/auth/AuthService.ts
@@ -3524,12 +3704,12 @@ var __metadata18 = function(k, v) {
3524
3704
  };
3525
3705
  var _a12;
3526
3706
  var _b10;
3527
- var _c7;
3528
- var _d6;
3707
+ var _c9;
3708
+ var _d7;
3529
3709
  var _e5;
3530
3710
  var _f4;
3531
3711
  var _g3;
3532
- var _h2;
3712
+ var _h3;
3533
3713
  var _j2;
3534
3714
  var _k;
3535
3715
  var _l;
@@ -3623,7 +3803,7 @@ var AuthService = class AuthService2 {
3623
3803
  * Nothing here logs the token, the link, the message body, or the recipient.
3624
3804
  */
3625
3805
  async deliverInvitation(userId, email2, userName, role) {
3626
- const { token, jti } = await this.tokenService.generateInviteToken(userId);
3806
+ const { token, jti } = await this.tokenService.generateInviteToken(userId, email2);
3627
3807
  const inviteLink = `${this.config.frontendUrl}/reset-password?token=${token}`;
3628
3808
  const accountType = role?.trim().toLowerCase() || void 0;
3629
3809
  const accountLabel = accountType ? `${accountType} account` : "account";
@@ -3817,6 +3997,7 @@ var AuthService = class AuthService2 {
3817
3997
  await this.userService.resetFailedAttempts(user.id);
3818
3998
  }
3819
3999
  const { password: _, failedLoginAttempts: __, lockoutUntil: ___, ...sanitized } = user;
4000
+ verifiedCredentials.set(sanitized, storedHash);
3820
4001
  return { user: sanitized, requirement };
3821
4002
  }
3822
4003
  /**
@@ -3956,12 +4137,12 @@ var AuthService = class AuthService2 {
3956
4137
  return result;
3957
4138
  }
3958
4139
  async forgotPassword(email2) {
3959
- const user = await this.userService.findByEmail(email2);
4140
+ const user = await this.userService.findByEmailInsensitive(email2);
3960
4141
  if (user) {
3961
- const { token } = await this.tokenService.generateResetToken(user.id);
3962
- const resetLink = `${this.config.frontendUrl}/reset-password?token=${token}`;
3963
4142
  try {
3964
- await this.emailService.sendHtml(email2, this.t("emails.passwordReset.subject"), passwordResetTemplate({
4143
+ const { token } = await this.tokenService.generateResetToken(user.id, user.email);
4144
+ const resetLink = `${this.config.frontendUrl}/reset-password?token=${token}`;
4145
+ await this.emailService.sendHtml(user.email, this.t("emails.passwordReset.subject"), passwordResetTemplate({
3965
4146
  resetLink,
3966
4147
  userName: user.name || email2
3967
4148
  }));
@@ -3981,7 +4162,7 @@ var AuthService = class AuthService2 {
3981
4162
  Err12(this.t("errors.invalidCredentials"), 401);
3982
4163
  }
3983
4164
  this.userValidator.validatePasswordStrength(newPassword);
3984
- await this.userService.update(userId, { password: newPassword });
4165
+ await this.userService.update(userId, { password: newPassword }, { expectedCredential: user });
3985
4166
  await this.tokenService.invalidateUserAccessTokens(userId);
3986
4167
  await this.tokenService.revokeAllForUser(userId);
3987
4168
  this.cookieManager.clearRefreshToken();
@@ -3991,7 +4172,10 @@ var AuthService = class AuthService2 {
3991
4172
  async resetPassword(token, newPassword) {
3992
4173
  this.userValidator.validatePasswordStrength(newPassword);
3993
4174
  const consumed = await this.tokenService.consumeSetPasswordToken(token);
3994
- const user = await this.userService.getById(consumed.userId);
4175
+ const user = await this.userService.getAuthRecordById(consumed.userId);
4176
+ if (!user || credentialFingerprint(this.config.jwt.refreshSecret, user) !== consumed.credentialState) {
4177
+ Err12(this.t("errors.invalidResetToken"), 401);
4178
+ }
3995
4179
  const acceptsInvitation = consumed.type === "invite";
3996
4180
  await this.userService.update(consumed.userId, {
3997
4181
  password: newPassword,
@@ -3999,7 +4183,7 @@ var AuthService = class AuthService2 {
3999
4183
  emailVerified: true,
4000
4184
  ...user.status === "pending" ? { status: "active" } : {}
4001
4185
  } : {}
4002
- });
4186
+ }, { expectedCredential: user });
4003
4187
  await this.tokenService.invalidateUserAccessTokens(consumed.userId);
4004
4188
  await this.tokenService.revokeAllForUser(consumed.userId);
4005
4189
  this.cookieManager.clearRefreshToken();
@@ -4075,7 +4259,7 @@ var AuthService = class AuthService2 {
4075
4259
  if (!email2) {
4076
4260
  Err12("This account has no email address to send a password reset to", 409);
4077
4261
  }
4078
- const { token, jti } = await this.tokenService.generateResetToken(user.id);
4262
+ const { token, jti } = await this.tokenService.generateResetToken(user.id, user.email);
4079
4263
  const resetLink = `${this.config.frontendUrl}/reset-password?token=${token}`;
4080
4264
  let emailSent = false;
4081
4265
  try {
@@ -4137,7 +4321,7 @@ var AuthService = class AuthService2 {
4137
4321
  }
4138
4322
  };
4139
4323
  __decorate18([
4140
- Inject12(AUTH_CONFIG),
4324
+ Inject13(AUTH_CONFIG),
4141
4325
  __metadata18("design:type", Object)
4142
4326
  ], AuthService.prototype, "config", void 0);
4143
4327
  __decorate18([
@@ -4162,7 +4346,7 @@ __decorate18([
4162
4346
  ], AuthService.prototype, "resetToTemporaryCredential", null);
4163
4347
  AuthService = __decorate18([
4164
4348
  Injectable12(),
4165
- __metadata18("design:paramtypes", [typeof (_a12 = typeof TokenService !== "undefined" && TokenService) === "function" ? _a12 : Object, typeof (_b10 = typeof UserService !== "undefined" && UserService) === "function" ? _b10 : Object, typeof (_c7 = typeof UserValidator !== "undefined" && UserValidator) === "function" ? _c7 : Object, typeof (_d6 = typeof EncryptionService !== "undefined" && EncryptionService) === "function" ? _d6 : Object, typeof (_e5 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _e5 : Object, typeof (_f4 = typeof I18nService2 !== "undefined" && I18nService2) === "function" ? _f4 : Object, typeof (_g3 = typeof EmailService !== "undefined" && EmailService) === "function" ? _g3 : Object, typeof (_h2 = typeof AuthSessionService !== "undefined" && AuthSessionService) === "function" ? _h2 : Object, typeof (_j2 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _j2 : Object, typeof (_k = typeof PasswordSetupService !== "undefined" && PasswordSetupService) === "function" ? _k : Object])
4349
+ __metadata18("design:paramtypes", [typeof (_a12 = typeof TokenService !== "undefined" && TokenService) === "function" ? _a12 : Object, typeof (_b10 = typeof UserService !== "undefined" && UserService) === "function" ? _b10 : Object, typeof (_c9 = typeof UserValidator !== "undefined" && UserValidator) === "function" ? _c9 : Object, typeof (_d7 = typeof EncryptionService !== "undefined" && EncryptionService) === "function" ? _d7 : Object, typeof (_e5 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _e5 : Object, typeof (_f4 = typeof I18nService2 !== "undefined" && I18nService2) === "function" ? _f4 : Object, typeof (_g3 = typeof EmailService !== "undefined" && EmailService) === "function" ? _g3 : Object, typeof (_h3 = typeof AuthSessionService !== "undefined" && AuthSessionService) === "function" ? _h3 : Object, typeof (_j2 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _j2 : Object, typeof (_k = typeof PasswordSetupService !== "undefined" && PasswordSetupService) === "function" ? _k : Object])
4166
4350
  ], AuthService);
4167
4351
 
4168
4352
  // src/auth/AuthGuard.ts
@@ -4394,7 +4578,7 @@ var loginDto = z.union([
4394
4578
  })
4395
4579
  ]);
4396
4580
  var changePasswordDto = z.object({
4397
- currentPassword: passwordField,
4581
+ currentPassword: loginCredentialField,
4398
4582
  newPassword: passwordField
4399
4583
  });
4400
4584
  var resetPasswordDto = z.object({
@@ -4617,7 +4801,7 @@ AuthController = __decorate21([
4617
4801
  ], AuthController);
4618
4802
 
4619
4803
  // src/auth/AuthResolver.ts
4620
- import { APP, Container, DI, Inject as Inject13, LOGGER, Meta, Service as Service4 } from "najm-core";
4804
+ import { APP, Container, DI, Inject as Inject14, LOGGER, Meta, Service as Service4 } from "najm-core";
4621
4805
  import { USER, ROLE, PERMISSIONS } from "najm-guard";
4622
4806
  var __decorate22 = function(decorators, target, key, desc) {
4623
4807
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -4755,11 +4939,11 @@ __decorate22([
4755
4939
  __metadata22("design:type", typeof (_a14 = typeof Container !== "undefined" && Container) === "function" ? _a14 : Object)
4756
4940
  ], AuthResolver.prototype, "container", void 0);
4757
4941
  __decorate22([
4758
- Inject13(APP),
4942
+ Inject14(APP),
4759
4943
  __metadata22("design:type", Object)
4760
4944
  ], AuthResolver.prototype, "app", void 0);
4761
4945
  __decorate22([
4762
- Inject13(LOGGER),
4946
+ Inject14(LOGGER),
4763
4947
  __metadata22("design:type", Object)
4764
4948
  ], AuthResolver.prototype, "log", void 0);
4765
4949
  AuthResolver = __decorate22([
@@ -4768,7 +4952,7 @@ AuthResolver = __decorate22([
4768
4952
  ], AuthResolver);
4769
4953
 
4770
4954
  // src/auth/AuthIdentityContextService.ts
4771
- import { DI as DI2, Inject as Inject14, INJECTION_TYPES, Meta as Meta2, Service as Service5 } from "najm-core";
4955
+ import { DI as DI2, Inject as Inject15, INJECTION_TYPES, Meta as Meta2, Service as Service5 } from "najm-core";
4772
4956
  var __decorate23 = function(decorators, target, key, desc) {
4773
4957
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4774
4958
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -4812,12 +4996,12 @@ __decorate23([
4812
4996
  AuthIdentityContextService = __decorate23([
4813
4997
  Service5(),
4814
4998
  Meta2({ layer: "plugin", order: 14 }),
4815
- __param6(0, Inject14(AUTH_CONFIG)),
4999
+ __param6(0, Inject15(AUTH_CONFIG)),
4816
5000
  __metadata23("design:paramtypes", [Object])
4817
5001
  ], AuthIdentityContextService);
4818
5002
 
4819
5003
  // src/auth/RegistrationController.ts
4820
- import { Body as Body2, Controller as Controller2, Err as Err14, Inject as Inject15, Post as Post2, ResMsg as ResMsg2 } from "najm-core";
5004
+ import { Body as Body2, Controller as Controller2, Err as Err14, Inject as Inject16, Post as Post2, ResMsg as ResMsg2 } from "najm-core";
4821
5005
  import { RateLimit as RateLimit2 } from "najm-rate";
4822
5006
  import { Public as Public2 } from "najm-guard";
4823
5007
  import { Validate as Validate2 } from "najm-validation";
@@ -4852,7 +5036,7 @@ var RegistrationController = class RegistrationController2 {
4852
5036
  }
4853
5037
  };
4854
5038
  __decorate24([
4855
- Inject15(AUTH_CONFIG),
5039
+ Inject16(AUTH_CONFIG),
4856
5040
  __metadata24("design:type", Object)
4857
5041
  ], RegistrationController.prototype, "config", void 0);
4858
5042
  __decorate24([
@@ -5320,8 +5504,8 @@ __export(permissions_exports, {
5320
5504
  });
5321
5505
 
5322
5506
  // src/permissions/PermissionRepository.ts
5323
- import { eq as eq7, and as and4 } from "drizzle-orm";
5324
- import { Repository as Repository6, Inject as Inject16 } from "najm-core";
5507
+ import { eq as eq7, and as and5, sql as sql4 } from "drizzle-orm";
5508
+ import { Repository as Repository6, Inject as Inject17 } from "najm-core";
5325
5509
  import { DB as DB6 } from "najm-database";
5326
5510
  var __decorate27 = function(decorators, target, key, desc) {
5327
5511
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -5389,16 +5573,58 @@ var PermissionRepository = class PermissionRepository2 {
5389
5573
  description: this.roles.description
5390
5574
  }).from(this.rolePermissions).leftJoin(this.roles, eq7(this.rolePermissions.roleId, this.roles.id)).where(eq7(this.rolePermissions.permissionId, permissionId));
5391
5575
  }
5576
+ /**
5577
+ * Capture grants and mutate their parent in one database transaction. A
5578
+ * PostgreSQL parent-row lock conflicts with the FK check of a concurrent
5579
+ * grant. SQLite executes the whole callback synchronously under its write
5580
+ * lock; an async callback would commit before its awaited queries finish.
5581
+ */
5582
+ async mutateWithRoles(id, mutation) {
5583
+ const grants = /* @__PURE__ */ __name((db) => {
5584
+ const query = db.selectDistinct({ roleId: this.rolePermissions.roleId }).from(this.rolePermissions);
5585
+ return id === void 0 ? query : query.where(eq7(this.rolePermissions.permissionId, id));
5586
+ }, "grants");
5587
+ const probe = this.db.select().from(this.permissions).limit(0);
5588
+ const sqlite = typeof probe.for !== "function";
5589
+ if (sqlite && this.db.resultKind === "sync") {
5590
+ return this.db.transaction((tx) => {
5591
+ const roleIds = grants(tx).all().map((row) => row.roleId);
5592
+ const rows = mutation(tx).returning().all();
5593
+ return { rows, roleIds };
5594
+ });
5595
+ }
5596
+ return this.db.transaction(async (tx) => {
5597
+ if (!sqlite && id === void 0) {
5598
+ await tx.execute(sql4`LOCK TABLE ${this.permissions} IN EXCLUSIVE MODE`);
5599
+ } else if (!sqlite) {
5600
+ await tx.select({ id: this.permissions.id }).from(this.permissions).where(eq7(this.permissions.id, id)).for("update");
5601
+ }
5602
+ const roleIds = (await grants(tx)).map((row) => row.roleId);
5603
+ const rows = await mutation(tx).returning();
5604
+ return { rows, roleIds };
5605
+ });
5606
+ }
5607
+ async updateWithRoles(id, data) {
5608
+ const { rows, roleIds } = await this.mutateWithRoles(id, (db) => db.update(this.permissions).set(data).where(eq7(this.permissions.id, id)));
5609
+ return { permission: rows[0], roleIds };
5610
+ }
5611
+ async deleteWithRoles(id) {
5612
+ const { rows, roleIds } = await this.mutateWithRoles(id, (db) => db.delete(this.permissions).where(eq7(this.permissions.id, id)));
5613
+ return { permission: rows[0], roleIds };
5614
+ }
5615
+ async deleteAllWithRoles() {
5616
+ return this.mutateWithRoles(void 0, (db) => db.delete(this.permissions));
5617
+ }
5392
5618
  async assignPermissionToRole(roleId, permissionId) {
5393
5619
  const [newRolePermission] = await this.db.insert(this.rolePermissions).values({ roleId, permissionId }).returning();
5394
5620
  return newRolePermission;
5395
5621
  }
5396
5622
  async removePermissionFromRole(roleId, permissionId) {
5397
- const [deletedRolePermission] = await this.db.delete(this.rolePermissions).where(and4(eq7(this.rolePermissions.roleId, roleId), eq7(this.rolePermissions.permissionId, permissionId))).returning();
5623
+ const [deletedRolePermission] = await this.db.delete(this.rolePermissions).where(and5(eq7(this.rolePermissions.roleId, roleId), eq7(this.rolePermissions.permissionId, permissionId))).returning();
5398
5624
  return deletedRolePermission;
5399
5625
  }
5400
5626
  async checkRoleHasPermission(roleId, permissionId) {
5401
- const [rolePermission] = await this.db.select().from(this.rolePermissions).where(and4(eq7(this.rolePermissions.roleId, roleId), eq7(this.rolePermissions.permissionId, permissionId)));
5627
+ const [rolePermission] = await this.db.select().from(this.rolePermissions).where(and5(eq7(this.rolePermissions.roleId, roleId), eq7(this.rolePermissions.permissionId, permissionId)));
5402
5628
  return !!rolePermission;
5403
5629
  }
5404
5630
  async deleteAll() {
@@ -5412,7 +5638,7 @@ __decorate27([
5412
5638
  __metadata27("design:type", Object)
5413
5639
  ], PermissionRepository.prototype, "db", void 0);
5414
5640
  __decorate27([
5415
- Inject16(AUTH_SCHEMA),
5641
+ Inject17(AUTH_SCHEMA),
5416
5642
  __metadata27("design:type", Object)
5417
5643
  ], PermissionRepository.prototype, "schema", void 0);
5418
5644
  PermissionRepository = __decorate27([
@@ -5588,8 +5814,8 @@ var __metadata30 = function(k, v) {
5588
5814
  };
5589
5815
  var _a19;
5590
5816
  var _b12;
5591
- var _c8;
5592
- var _d7;
5817
+ var _c10;
5818
+ var _d8;
5593
5819
  var _e6;
5594
5820
  var PermissionService = class PermissionService2 {
5595
5821
  static {
@@ -5615,10 +5841,14 @@ var PermissionService = class PermissionService2 {
5615
5841
  * sessions that captured it end. This is an infrequent administrative
5616
5842
  * action, and the work is proportional to the role's membership.
5617
5843
  */
5618
- async invalidateRoleHolders(roleId) {
5844
+ async invalidateRoleHolders(roleIds) {
5619
5845
  if (!this.userRepository || !this.sessionInvalidation)
5620
5846
  return;
5621
- const userIds = await this.userRepository.getIdsByRole(roleId);
5847
+ const userIds = /* @__PURE__ */ new Set();
5848
+ for (const roleId of new Set(typeof roleIds === "string" ? [roleIds] : roleIds)) {
5849
+ for (const userId of await this.userRepository.getIdsByRole(roleId))
5850
+ userIds.add(userId);
5851
+ }
5622
5852
  for (const userId of userIds) {
5623
5853
  await this.sessionInvalidation.invalidateAccessTokens(userId);
5624
5854
  }
@@ -5643,11 +5873,18 @@ var PermissionService = class PermissionService2 {
5643
5873
  async update(id, data) {
5644
5874
  await this.permissionValidator.checkPermissionExists(id);
5645
5875
  await this.permissionValidator.checkPermissionNameUnique(data.name, id);
5646
- return await this.permissionRepository.update(id, data);
5876
+ const changesClaims = ["name", "resource", "action"].some((field) => data[field] !== void 0);
5877
+ if (!changesClaims)
5878
+ return this.permissionRepository.update(id, data);
5879
+ const { permission: updated, roleIds } = await this.permissionRepository.updateWithRoles(id, data);
5880
+ await this.invalidateRoleHolders(roleIds);
5881
+ return updated;
5647
5882
  }
5648
5883
  async delete(id) {
5649
5884
  await this.permissionValidator.checkPermissionExists(id);
5650
- return await this.permissionRepository.delete(id);
5885
+ const { permission: deleted, roleIds } = await this.permissionRepository.deleteWithRoles(id);
5886
+ await this.invalidateRoleHolders(roleIds);
5887
+ return deleted;
5651
5888
  }
5652
5889
  async getPermissionsByRole(roleId) {
5653
5890
  return await this.permissionRepository.getPermissionsByRole(roleId);
@@ -5716,12 +5953,14 @@ var PermissionService = class PermissionService2 {
5716
5953
  return { assigned, skipped };
5717
5954
  }
5718
5955
  async deleteAll() {
5719
- return await this.permissionRepository.deleteAll();
5956
+ const { rows: deleted, roleIds } = await this.permissionRepository.deleteAllWithRoles();
5957
+ await this.invalidateRoleHolders(roleIds);
5958
+ return deleted;
5720
5959
  }
5721
5960
  };
5722
5961
  PermissionService = __decorate30([
5723
5962
  Injectable15(),
5724
- __metadata30("design:paramtypes", [typeof (_a19 = typeof PermissionRepository !== "undefined" && PermissionRepository) === "function" ? _a19 : Object, typeof (_b12 = typeof PermissionValidator !== "undefined" && PermissionValidator) === "function" ? _b12 : Object, typeof (_c8 = typeof RoleService !== "undefined" && RoleService) === "function" ? _c8 : Object, typeof (_d7 = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _d7 : Object, typeof (_e6 = typeof SessionInvalidationService !== "undefined" && SessionInvalidationService) === "function" ? _e6 : Object])
5963
+ __metadata30("design:paramtypes", [typeof (_a19 = typeof PermissionRepository !== "undefined" && PermissionRepository) === "function" ? _a19 : Object, typeof (_b12 = typeof PermissionValidator !== "undefined" && PermissionValidator) === "function" ? _b12 : Object, typeof (_c10 = typeof RoleService !== "undefined" && RoleService) === "function" ? _c10 : Object, typeof (_d8 = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _d8 : Object, typeof (_e6 = typeof SessionInvalidationService !== "undefined" && SessionInvalidationService) === "function" ? _e6 : Object])
5725
5964
  ], PermissionService);
5726
5965
 
5727
5966
  // src/permissions/PermissionController.ts
@@ -5954,7 +6193,7 @@ var revokeTokenDto = z4.object({
5954
6193
  });
5955
6194
 
5956
6195
  // src/ownership/scopedOwnership.ts
5957
- import { aliasedTable, eq as eq8, getTableColumns, sql as sql4 } from "drizzle-orm";
6196
+ import { aliasedTable, eq as eq8, getTableColumns, sql as sql5 } from "drizzle-orm";
5958
6197
  var DEFAULT_ADMIN_ROLES = ["admin"];
5959
6198
  var DRIZZLE_NAME = Symbol.for("drizzle:Name");
5960
6199
  var DRIZZLE_BASE_NAME = Symbol.for("drizzle:BaseName");
@@ -6091,7 +6330,7 @@ var OwnershipToken = class {
6091
6330
  return query;
6092
6331
  const rule = this._rules[role];
6093
6332
  if (!rule)
6094
- return query.where(sql4`1 = 0`);
6333
+ return query.where(sql5`1 = 0`);
6095
6334
  const { query: q, condition } = rule(uid, query);
6096
6335
  return q.where(condition);
6097
6336
  }
@@ -6106,7 +6345,7 @@ var OwnershipToken = class {
6106
6345
  return { query, condition: null };
6107
6346
  const rule = this._rules[role];
6108
6347
  if (!rule)
6109
- return { query, condition: sql4`1 = 0` };
6348
+ return { query, condition: sql5`1 = 0` };
6110
6349
  return rule(uid, query);
6111
6350
  }
6112
6351
  };
@@ -6116,7 +6355,7 @@ function own(table, opts) {
6116
6355
  __name(own, "own");
6117
6356
 
6118
6357
  // src/ownership/configureOwnership.ts
6119
- import { Injectable as Injectable16, Inject as Inject17, User as User4, Body as Body6, Params as Params4 } from "najm-core";
6358
+ import { Injectable as Injectable16, Inject as Inject18, User as User4, Body as Body6, Params as Params4 } from "najm-core";
6120
6359
  import { createGuard as createGuard5, composeGuards as composeGuards4 } from "najm-guard";
6121
6360
  var __decorate32 = function(decorators, target, key, desc) {
6122
6361
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -6157,7 +6396,7 @@ function createResourceGuards(ownershipClass, resourceType, resource, options) {
6157
6396
  }
6158
6397
  };
6159
6398
  __decorate32([
6160
- Inject17(ownershipClass),
6399
+ Inject18(ownershipClass),
6161
6400
  __metadata32("design:type", Object)
6162
6401
  ], AccessGuard.prototype, "ownership", void 0);
6163
6402
  __decorate32([
@@ -6181,7 +6420,7 @@ function createResourceGuards(ownershipClass, resourceType, resource, options) {
6181
6420
  }
6182
6421
  };
6183
6422
  __decorate32([
6184
- Inject17(ownershipClass),
6423
+ Inject18(ownershipClass),
6185
6424
  __metadata32("design:type", Object)
6186
6425
  ], ListGuard.prototype, "ownership", void 0);
6187
6426
  __decorate32([
@@ -6340,7 +6579,7 @@ function configureOwnership(config) {
6340
6579
  }
6341
6580
  };
6342
6581
  __decorate32([
6343
- Inject17(GeneratedOwnershipService),
6582
+ Inject18(GeneratedOwnershipService),
6344
6583
  __metadata32("design:type", GeneratedOwnershipService)
6345
6584
  ], BodyAccessGuard.prototype, "ownership", void 0);
6346
6585
  __decorate32([
@@ -6465,12 +6704,12 @@ __name(Policy, "Policy");
6465
6704
 
6466
6705
  // src/ownership/OwnedDecorator.ts
6467
6706
  import "reflect-metadata";
6468
- import { sql as sql6, and as and5 } from "drizzle-orm";
6469
- import { Injectable as Injectable17, Inject as Inject18, DI as DI3, Container as Container2, REQUEST_ID } from "najm-core";
6707
+ import { sql as sql7, and as and6 } from "drizzle-orm";
6708
+ import { Injectable as Injectable17, Inject as Inject19, DI as DI3, Container as Container2, REQUEST_ID } from "najm-core";
6470
6709
  import { USER as USER2 } from "najm-guard";
6471
6710
 
6472
6711
  // src/ownership/ownershipCondition.ts
6473
- import { inArray, or, sql as sql5 } from "drizzle-orm";
6712
+ import { inArray, or, sql as sql6 } from "drizzle-orm";
6474
6713
  function validateOwnershipAlternatives(tokens) {
6475
6714
  if (!tokens.length)
6476
6715
  throw new Error("Ownership requires at least one token");
@@ -6489,7 +6728,7 @@ function ownershipCondition(db, tokens, context) {
6489
6728
  return void 0;
6490
6729
  const user = context.getUser();
6491
6730
  if (!user)
6492
- return sql5`1 = 0`;
6731
+ return sql6`1 = 0`;
6493
6732
  const conditions = [];
6494
6733
  for (const token of tokens) {
6495
6734
  const { query, condition } = token.applyScopeSplit(user.id, user.role, db.select({ id: idColumn }).from(token.table));
@@ -6500,7 +6739,7 @@ function ownershipCondition(db, tokens, context) {
6500
6739
  conditions.push(inArray(idColumn, query.where(condition)));
6501
6740
  }
6502
6741
  if (!conditions.length)
6503
- return sql5`1 = 0`;
6742
+ return sql6`1 = 0`;
6504
6743
  return conditions.length === 1 ? conditions[0] : or(...conditions);
6505
6744
  }
6506
6745
  __name(ownershipCondition, "ownershipCondition");
@@ -6531,7 +6770,7 @@ function ownedProperty(proto, key, tokens) {
6531
6770
  enumerable: false
6532
6771
  });
6533
6772
  if (!Reflect.hasOwnMetadata(OWNED_PROPERTIES, proto, "_scopeCtx")) {
6534
- Inject18(ScopeContext)(proto, "_scopeCtx");
6773
+ Inject19(ScopeContext)(proto, "_scopeCtx");
6535
6774
  Reflect.defineMetadata(OWNED_PROPERTIES, true, proto, "_scopeCtx");
6536
6775
  Object.defineProperty(proto, "_scopeCtx", {
6537
6776
  get() {
@@ -6605,7 +6844,7 @@ function Owned(token, ...alternatives) {
6605
6844
  throw new Error("@Owned requires a repository class");
6606
6845
  Reflect.defineMetadata(OWNED_META, token, target);
6607
6846
  const proto = target.prototype;
6608
- Inject18(ScopeContext)(proto, "_scopeCtx");
6847
+ Inject19(ScopeContext)(proto, "_scopeCtx");
6609
6848
  Object.defineProperty(proto, "ownershipCondition", {
6610
6849
  value() {
6611
6850
  return ownershipCondition(this.db, tokens, this._scopeCtx);
@@ -6627,7 +6866,7 @@ function Owned(token, ...alternatives) {
6627
6866
  return null;
6628
6867
  if (filtered.length === 1)
6629
6868
  return filtered[0];
6630
- return and5(...filtered);
6869
+ return and6(...filtered);
6631
6870
  }
6632
6871
  __name(combineConditions, "combineConditions");
6633
6872
  function applyQueryOptions(query, opts = {}, whereCondition) {
@@ -6648,7 +6887,7 @@ function Owned(token, ...alternatives) {
6648
6887
  return query.where(this.ownershipCondition());
6649
6888
  const user = getUser(this);
6650
6889
  if (!user)
6651
- return query.where(sql6`1 = 0`);
6890
+ return query.where(sql7`1 = 0`);
6652
6891
  return token.applyScope(user.id, user.role, query);
6653
6892
  };
6654
6893
  },
@@ -6827,7 +7066,7 @@ __name(getAuthLocale, "getAuthLocale");
6827
7066
  var AUTH_SUPPORTED_LANGUAGES = Object.keys(AUTH_LOCALES);
6828
7067
 
6829
7068
  // src/oauth/google/GoogleOAuthProvider.ts
6830
- import { Inject as Inject20, Injectable as Injectable19 } from "najm-core";
7069
+ import { Inject as Inject21, Injectable as Injectable19 } from "najm-core";
6831
7070
 
6832
7071
  // src/oauth/types.ts
6833
7072
  var OAuthFlowError = class extends Error {
@@ -6845,7 +7084,7 @@ var OAuthFlowError = class extends Error {
6845
7084
  };
6846
7085
 
6847
7086
  // src/oauth/google/GoogleTokenVerifier.ts
6848
- import { Inject as Inject19, Injectable as Injectable18 } from "najm-core";
7087
+ import { Inject as Inject20, Injectable as Injectable18 } from "najm-core";
6849
7088
  import { createRemoteJWKSet, jwtVerify } from "jose";
6850
7089
  var __decorate34 = function(decorators, target, key, desc) {
6851
7090
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -6906,7 +7145,7 @@ var GoogleTokenVerifier = class GoogleTokenVerifier2 {
6906
7145
  }
6907
7146
  };
6908
7147
  __decorate34([
6909
- Inject19(AUTH_CONFIG),
7148
+ Inject20(AUTH_CONFIG),
6910
7149
  __metadata34("design:type", Object)
6911
7150
  ], GoogleTokenVerifier.prototype, "config", void 0);
6912
7151
  GoogleTokenVerifier = __decorate34([
@@ -6992,7 +7231,7 @@ var GoogleOAuthProvider = class GoogleOAuthProvider2 {
6992
7231
  }
6993
7232
  };
6994
7233
  __decorate35([
6995
- Inject20(AUTH_CONFIG),
7234
+ Inject21(AUTH_CONFIG),
6996
7235
  __metadata35("design:type", Object)
6997
7236
  ], GoogleOAuthProvider.prototype, "config", void 0);
6998
7237
  GoogleOAuthProvider = __decorate35([
@@ -7001,7 +7240,7 @@ GoogleOAuthProvider = __decorate35([
7001
7240
  ], GoogleOAuthProvider);
7002
7241
 
7003
7242
  // src/oauth/github/GitHubOAuthProvider.ts
7004
- import { Inject as Inject21, Injectable as Injectable20 } from "najm-core";
7243
+ import { Inject as Inject22, Injectable as Injectable20 } from "najm-core";
7005
7244
  var __decorate36 = function(decorators, target, key, desc) {
7006
7245
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
7007
7246
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -7125,7 +7364,7 @@ var GitHubOAuthProvider = class GitHubOAuthProvider2 {
7125
7364
  }
7126
7365
  };
7127
7366
  __decorate36([
7128
- Inject21(AUTH_CONFIG),
7367
+ Inject22(AUTH_CONFIG),
7129
7368
  __metadata36("design:type", Object)
7130
7369
  ], GitHubOAuthProvider.prototype, "config", void 0);
7131
7370
  GitHubOAuthProvider = __decorate36([
@@ -7139,16 +7378,16 @@ import { RateLimit as RateLimit3 } from "najm-rate";
7139
7378
  import { Public as Public3 } from "najm-guard";
7140
7379
 
7141
7380
  // src/oauth/OAuthService.ts
7142
- import { Err as Err16, Inject as Inject24, Injectable as Injectable23, Log as Log2 } from "najm-core";
7381
+ import { Err as Err16, Inject as Inject25, Injectable as Injectable23, Log as Log2 } from "najm-core";
7143
7382
 
7144
7383
  // src/oauth/OAuthAccountService.ts
7145
7384
  import { randomBytes as randomBytes3 } from "crypto";
7146
- import { Inject as Inject23, Injectable as Injectable21 } from "najm-core";
7385
+ import { Inject as Inject24, Injectable as Injectable21 } from "najm-core";
7147
7386
  import { Transaction as Transaction5 } from "najm-database";
7148
7387
 
7149
7388
  // src/oauth/OAuthAccountRepository.ts
7150
- import { and as and6, eq as eq9 } from "drizzle-orm";
7151
- import { Inject as Inject22, Repository as Repository7 } from "najm-core";
7389
+ import { and as and7, eq as eq9 } from "drizzle-orm";
7390
+ import { Inject as Inject23, Repository as Repository7 } from "najm-core";
7152
7391
  import { DB as DB7 } from "najm-database";
7153
7392
  var __decorate37 = function(decorators, target, key, desc) {
7154
7393
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -7172,11 +7411,11 @@ var OAuthAccountRepository = class OAuthAccountRepository2 {
7172
7411
  return this.schema.oauthAccounts;
7173
7412
  }
7174
7413
  async getByProviderAccount(provider, providerAccountId) {
7175
- const [account] = await this.db.select().from(this.accounts).where(and6(eq9(this.accounts.provider, provider), eq9(this.accounts.providerAccountId, providerAccountId))).limit(1);
7414
+ const [account] = await this.db.select().from(this.accounts).where(and7(eq9(this.accounts.provider, provider), eq9(this.accounts.providerAccountId, providerAccountId))).limit(1);
7176
7415
  return account;
7177
7416
  }
7178
7417
  async getByUserProvider(userId, provider) {
7179
- const [account] = await this.db.select().from(this.accounts).where(and6(eq9(this.accounts.userId, userId), eq9(this.accounts.provider, provider))).limit(1);
7418
+ const [account] = await this.db.select().from(this.accounts).where(and7(eq9(this.accounts.userId, userId), eq9(this.accounts.provider, provider))).limit(1);
7180
7419
  return account;
7181
7420
  }
7182
7421
  async create(data) {
@@ -7189,7 +7428,7 @@ __decorate37([
7189
7428
  __metadata37("design:type", Object)
7190
7429
  ], OAuthAccountRepository.prototype, "db", void 0);
7191
7430
  __decorate37([
7192
- Inject22(AUTH_SCHEMA),
7431
+ Inject23(AUTH_SCHEMA),
7193
7432
  __metadata37("design:type", Object)
7194
7433
  ], OAuthAccountRepository.prototype, "schema", void 0);
7195
7434
  OAuthAccountRepository = __decorate37([
@@ -7208,8 +7447,8 @@ var __metadata38 = function(k, v) {
7208
7447
  };
7209
7448
  var _a23;
7210
7449
  var _b13;
7211
- var _c9;
7212
- var _d8;
7450
+ var _c11;
7451
+ var _d9;
7213
7452
  var OAuthAccountService = class OAuthAccountService2 {
7214
7453
  static {
7215
7454
  __name(this, "OAuthAccountService");
@@ -7286,20 +7525,20 @@ var OAuthAccountService = class OAuthAccountService2 {
7286
7525
  }
7287
7526
  };
7288
7527
  __decorate38([
7289
- Inject23(AUTH_CONFIG),
7528
+ Inject24(AUTH_CONFIG),
7290
7529
  __metadata38("design:type", Object)
7291
7530
  ], OAuthAccountService.prototype, "config", void 0);
7292
7531
  __decorate38([
7293
7532
  Transaction5(),
7294
7533
  __metadata38("design:type", Function),
7295
7534
  __metadata38("design:paramtypes", [Object]),
7296
- __metadata38("design:returntype", typeof (_c9 = typeof Promise !== "undefined" && Promise) === "function" ? _c9 : Object)
7535
+ __metadata38("design:returntype", typeof (_c11 = typeof Promise !== "undefined" && Promise) === "function" ? _c11 : Object)
7297
7536
  ], OAuthAccountService.prototype, "resolveForLogin", null);
7298
7537
  __decorate38([
7299
7538
  Transaction5(),
7300
7539
  __metadata38("design:type", Function),
7301
7540
  __metadata38("design:paramtypes", [String, Object]),
7302
- __metadata38("design:returntype", typeof (_d8 = typeof Promise !== "undefined" && Promise) === "function" ? _d8 : Object)
7541
+ __metadata38("design:returntype", typeof (_d9 = typeof Promise !== "undefined" && Promise) === "function" ? _d9 : Object)
7303
7542
  ], OAuthAccountService.prototype, "linkUser", null);
7304
7543
  OAuthAccountService = __decorate38([
7305
7544
  Injectable21(),
@@ -7310,6 +7549,22 @@ OAuthAccountService = __decorate38([
7310
7549
  import { createHash as createHash4, randomBytes as randomBytes4, timingSafeEqual } from "crypto";
7311
7550
  import { Injectable as Injectable22 } from "najm-core";
7312
7551
  import { CookieService as CookieService3 } from "najm-cookies";
7552
+
7553
+ // src/client/redirectPath.ts
7554
+ function normalizeLocalRedirectPath(value) {
7555
+ if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//") || value.includes("\\") || /[\u0000-\u001f\u007f]/.test(value)) return null;
7556
+ try {
7557
+ const base = new URL("https://najm.invalid");
7558
+ const parsed = new URL(value, base);
7559
+ if (parsed.origin !== base.origin || parsed.pathname.startsWith("//")) return null;
7560
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`;
7561
+ } catch {
7562
+ return null;
7563
+ }
7564
+ }
7565
+ __name(normalizeLocalRedirectPath, "normalizeLocalRedirectPath");
7566
+
7567
+ // src/oauth/OAuthStateService.ts
7313
7568
  var __decorate39 = function(decorators, target, key, desc) {
7314
7569
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
7315
7570
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -7385,22 +7640,10 @@ var OAuthStateService = class OAuthStateService2 {
7385
7640
  }
7386
7641
  }
7387
7642
  validateReturnTo(value) {
7388
- const candidate = value?.trim() || "/";
7389
- if (!candidate.startsWith("/") || candidate.startsWith("//") || candidate.includes("\\")) {
7390
- throw new OAuthFlowError("oauth_redirect_invalid");
7391
- }
7392
- try {
7393
- const base = new URL("https://najm.invalid");
7394
- const parsed = new URL(candidate, base);
7395
- if (parsed.origin !== base.origin || parsed.username || parsed.password) {
7396
- throw new OAuthFlowError("oauth_redirect_invalid");
7397
- }
7398
- return `${parsed.pathname}${parsed.search}${parsed.hash}`;
7399
- } catch (error) {
7400
- if (error instanceof OAuthFlowError)
7401
- throw error;
7643
+ const path2 = normalizeLocalRedirectPath(value?.trim() || "/");
7644
+ if (!path2)
7402
7645
  throw new OAuthFlowError("oauth_redirect_invalid");
7403
- }
7646
+ return path2;
7404
7647
  }
7405
7648
  cookieName(provider, state) {
7406
7649
  return `${COOKIE_PREFIX}${provider}.${state}`;
@@ -7426,12 +7669,12 @@ var __metadata40 = function(k, v) {
7426
7669
  };
7427
7670
  var _a25;
7428
7671
  var _b15;
7429
- var _c10;
7430
- var _d9;
7672
+ var _c12;
7673
+ var _d10;
7431
7674
  var _e7;
7432
7675
  var _f5;
7433
7676
  var _g4;
7434
- var _h3;
7677
+ var _h4;
7435
7678
  var OAuthService = class OAuthService2 {
7436
7679
  static {
7437
7680
  __name(this, "OAuthService");
@@ -7597,7 +7840,7 @@ var OAuthService = class OAuthService2 {
7597
7840
  }
7598
7841
  };
7599
7842
  __decorate40([
7600
- Inject24(AUTH_CONFIG),
7843
+ Inject25(AUTH_CONFIG),
7601
7844
  __metadata40("design:type", Object)
7602
7845
  ], OAuthService.prototype, "config", void 0);
7603
7846
  __decorate40([
@@ -7606,7 +7849,7 @@ __decorate40([
7606
7849
  ], OAuthService.prototype, "logger", void 0);
7607
7850
  OAuthService = __decorate40([
7608
7851
  Injectable23(),
7609
- __metadata40("design:paramtypes", [typeof (_a25 = typeof OAuthStateService !== "undefined" && OAuthStateService) === "function" ? _a25 : Object, typeof (_b15 = typeof GoogleOAuthProvider !== "undefined" && GoogleOAuthProvider) === "function" ? _b15 : Object, typeof (_c10 = typeof GitHubOAuthProvider !== "undefined" && GitHubOAuthProvider) === "function" ? _c10 : Object, typeof (_d9 = typeof OAuthAccountService !== "undefined" && OAuthAccountService) === "function" ? _d9 : Object, typeof (_e7 = typeof AuthSessionService !== "undefined" && AuthSessionService) === "function" ? _e7 : Object, typeof (_f5 = typeof TokenService !== "undefined" && TokenService) === "function" ? _f5 : Object, typeof (_g4 = typeof UserService !== "undefined" && UserService) === "function" ? _g4 : Object, typeof (_h3 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _h3 : Object])
7852
+ __metadata40("design:paramtypes", [typeof (_a25 = typeof OAuthStateService !== "undefined" && OAuthStateService) === "function" ? _a25 : Object, typeof (_b15 = typeof GoogleOAuthProvider !== "undefined" && GoogleOAuthProvider) === "function" ? _b15 : Object, typeof (_c12 = typeof GitHubOAuthProvider !== "undefined" && GitHubOAuthProvider) === "function" ? _c12 : Object, typeof (_d10 = typeof OAuthAccountService !== "undefined" && OAuthAccountService) === "function" ? _d10 : Object, typeof (_e7 = typeof AuthSessionService !== "undefined" && AuthSessionService) === "function" ? _e7 : Object, typeof (_f5 = typeof TokenService !== "undefined" && TokenService) === "function" ? _f5 : Object, typeof (_g4 = typeof UserService !== "undefined" && UserService) === "function" ? _g4 : Object, typeof (_h4 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _h4 : Object])
7610
7853
  ], OAuthService);
7611
7854
 
7612
7855
  // src/oauth/GitHubOAuthController.ts