@nxgt/janus 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +116 -23
  2. package/dist/auth/config.d.ts +3 -0
  3. package/dist/auth/config.d.ts.map +1 -1
  4. package/dist/auth/context.d.ts +12 -0
  5. package/dist/auth/context.d.ts.map +1 -1
  6. package/dist/auth/email-flows.d.ts +11 -0
  7. package/dist/auth/email-flows.d.ts.map +1 -0
  8. package/dist/auth/index.d.ts +1 -1
  9. package/dist/auth/index.d.ts.map +1 -1
  10. package/dist/auth/one-time.d.ts +66 -6
  11. package/dist/auth/one-time.d.ts.map +1 -1
  12. package/dist/auth/password-written.d.ts +24 -0
  13. package/dist/auth/password-written.d.ts.map +1 -0
  14. package/dist/auth/port/assert-stores.d.ts.map +1 -1
  15. package/dist/auth/port/types.d.ts +23 -1
  16. package/dist/auth/port/types.d.ts.map +1 -1
  17. package/dist/auth/second-factor/challenge.d.ts +0 -6
  18. package/dist/auth/second-factor/challenge.d.ts.map +1 -1
  19. package/dist/auth/second-factor/factor.d.ts +1 -4
  20. package/dist/auth/second-factor/factor.d.ts.map +1 -1
  21. package/dist/auth/second-factor/flows.d.ts +7 -3
  22. package/dist/auth/second-factor/flows.d.ts.map +1 -1
  23. package/dist/auth/second-factor/lifecycle.d.ts.map +1 -1
  24. package/dist/auth/sign-in-code.d.ts +20 -0
  25. package/dist/auth/sign-in-code.d.ts.map +1 -0
  26. package/dist/auth/types.d.ts +56 -1
  27. package/dist/auth/types.d.ts.map +1 -1
  28. package/dist/auth/users.d.ts +2 -2
  29. package/dist/auth/users.d.ts.map +1 -1
  30. package/dist/chunks/{index-gbwn2tts.js → index-c0jajeda.js} +12 -2
  31. package/dist/chunks/{index-gbwn2tts.js.map → index-c0jajeda.js.map} +3 -3
  32. package/dist/conformance/cases/outage.d.ts.map +1 -1
  33. package/dist/conformance/cases/tokens.d.ts.map +1 -1
  34. package/dist/conformance/index.js +73 -2
  35. package/dist/conformance/index.js.map +4 -4
  36. package/dist/index.js +253 -116
  37. package/dist/index.js.map +15 -12
  38. package/docs/README.md +2 -1
  39. package/docs/guide/adapters.md +56 -3
  40. package/docs/guide/email-flows.md +11 -1
  41. package/docs/guide/errors.md +3 -3
  42. package/docs/guide/second-factor.md +31 -4
  43. package/docs/guide/sign-in-code.md +489 -0
  44. package/docs/guide/users.md +5 -4
  45. package/docs/guide/vocabulary.md +7 -6
  46. package/docs/roadmap.md +28 -13
  47. package/docs/troubleshooting.md +193 -28
  48. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  isId2,
12
12
  mintedAt2,
13
13
  createMemoryStores2
14
- } from "./chunks/index-gbwn2tts.js";
14
+ } from "./chunks/index-c0jajeda.js";
15
15
  import {
16
16
  JanusError2,
17
17
  StoreFailure2,
@@ -278,7 +278,8 @@ function resolveConfig(config, where) {
278
278
  types,
279
279
  tokenTtlMs: {
280
280
  verifyEmail: parseDuration(config.tokens?.verifyEmail ?? "24h", `${where}: tokens.verifyEmail`),
281
- resetPassword: parseDuration(config.tokens?.resetPassword ?? "1h", `${where}: tokens.resetPassword`)
281
+ resetPassword: parseDuration(config.tokens?.resetPassword ?? "1h", `${where}: tokens.resetPassword`),
282
+ signInCode: parseDuration(config.tokens?.signInCode ?? "10m", `${where}: tokens.signInCode`)
282
283
  },
283
284
  cookie: resolveCookie(config.cookie ?? {}, where),
284
285
  secondFactor: resolveSecondFactor(config.secondFactor, where)
@@ -513,6 +514,12 @@ function loginsOf(type, fields, where) {
513
514
  logins.add(normalizeEmail(email));
514
515
  return [...logins];
515
516
  }
517
+ function passwordRule(type, where) {
518
+ if (type.password === null) {
519
+ throw new TypeError(`${where}: the ${type.name} type does not sign in with a password — add password: { login } to it`);
520
+ }
521
+ return type.password;
522
+ }
516
523
  function checkPassword(type, password, where) {
517
524
  const minLength = type.password?.minLength ?? 8;
518
525
  if (typeof password !== "string" || password.length < minLength) {
@@ -579,6 +586,14 @@ async function writeUser(context, user, type, options, where, patchOf) {
579
586
  const patch = await patchOf(record, now);
580
587
  return context.store.users.updateUser(record.id, { ...patch, updatedAt: now }, record.version);
581
588
  }
589
+ async function holderOfEmail(context, type, email) {
590
+ const wanted = normalizeEmail(email);
591
+ if (!isStorable(wanted))
592
+ return null;
593
+ const record = await context.store.users.findUserByLogin(type.name, wanted);
594
+ const held = record === null ? null : emailOf(type, record.fields);
595
+ return held !== null && normalizeEmail(held) === wanted ? record : null;
596
+ }
582
597
 
583
598
  // src/auth/port/assert-stores.ts
584
599
  var REQUIRED = {
@@ -598,7 +613,13 @@ var REQUIRED = {
598
613
  "revokeUserSessions",
599
614
  "deleteUserSessions"
600
615
  ],
601
- tokens: ["insertToken", "consumeToken", "countAttempt", "deleteUserTokens"]
616
+ tokens: [
617
+ "insertToken",
618
+ "consumeToken",
619
+ "countAttempt",
620
+ "spendUserTokens",
621
+ "deleteUserTokens"
622
+ ]
602
623
  };
603
624
  function assertStores(stores, where) {
604
625
  if (typeof stores !== "object" || stores === null) {
@@ -782,6 +803,30 @@ function headersOf(request) {
782
803
  }
783
804
 
784
805
  // src/auth/one-time.ts
806
+ import { randomInt, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
807
+ var CODE_ATTEMPTS = 5;
808
+ async function countCodeAttempt(context, secret, kind, where, userType) {
809
+ const token = await context.store.tokens.countAttempt(hashSecret(secret), kind);
810
+ refuseUnusable(token, context.clock.now(), where, "challenge");
811
+ if (token.attempts > CODE_ATTEMPTS) {
812
+ throw codeInvalid(where, token.userId, userType, 0);
813
+ }
814
+ return { token, attemptsLeft: CODE_ATTEMPTS - token.attempts };
815
+ }
816
+ function codeInvalid(where, userId, userType, attemptsLeft) {
817
+ return new TokenError2("CODE_INVALID", `${where}: the code does not match, or was already used`, {
818
+ operation: where,
819
+ userId,
820
+ userType,
821
+ ...attemptsLeft === undefined ? {} : { attemptsLeft }
822
+ });
823
+ }
824
+ function refuseStale(type, user, token, where, noun) {
825
+ const email = emailOf(type, user.fields);
826
+ if (email === null || normalizeEmail(email) !== normalizeEmail(token.address)) {
827
+ throw new TokenError2("TOKEN_STALE", `${where}: the ${noun} was sent to an e-mail the user no longer has`, { operation: where, userId: user.id, userType: type.name });
828
+ }
829
+ }
785
830
  function refuseUnusable(token, now, where, noun) {
786
831
  if (token === null) {
787
832
  throw new TokenError2("TOKEN_UNKNOWN", `${where}: no such ${noun}`, {
@@ -800,13 +845,37 @@ function refuseUnusable(token, now, where, noun) {
800
845
  var unknownOneTime = (where, noun) => new TokenError2("TOKEN_UNKNOWN", `${where}: no such ${noun}`, {
801
846
  operation: where
802
847
  });
848
+ async function unknownChallenge(context, token, secret, where) {
849
+ if (token.attempts === CODE_ATTEMPTS) {
850
+ await burnOneTime(context, secret, token.kind);
851
+ }
852
+ return unknownOneTime(where, "challenge");
853
+ }
803
854
  async function spendOneTime(context, secret, kind, where, noun) {
804
855
  const now = context.clock.now();
805
856
  const token = await context.store.tokens.consumeToken(hashSecret(secret), kind, now);
806
857
  refuseUnusable(token, now, where, noun);
807
858
  return token;
808
859
  }
860
+ async function burnOneTime(context, secret, kind) {
861
+ await context.store.tokens.consumeToken(hashSecret(secret), kind, context.clock.now());
862
+ }
863
+ var hashCode = (challenge, code) => hashSecret(`${challenge}${code}`);
864
+ function codeMatches(token, challenge, code) {
865
+ if (token.codeHash === null || !/^\d{6}$/.test(code))
866
+ return false;
867
+ const given = Buffer.from(hashCode(challenge, code));
868
+ const held = Buffer.from(token.codeHash);
869
+ return given.length === held.length && timingSafeEqual2(given, held);
870
+ }
809
871
  async function issueOneTime(context, token) {
872
+ return insertOneTime(context, token, null);
873
+ }
874
+ async function issueCode(context, token) {
875
+ const code = String(randomInt(1e6)).padStart(6, "0");
876
+ return { ...await insertOneTime(context, token, code), code };
877
+ }
878
+ async function insertOneTime(context, token, code) {
810
879
  const now = context.clock.now();
811
880
  const secret = mintSecret();
812
881
  const expiresAt = new Date(now.getTime() + token.ttlMs);
@@ -815,7 +884,7 @@ async function issueOneTime(context, token) {
815
884
  kind: token.kind,
816
885
  userId: token.userId,
817
886
  address: token.address,
818
- codeHash: null,
887
+ codeHash: code === null ? null : hashCode(secret, code),
819
888
  attempts: 0,
820
889
  expiresAt,
821
890
  spentAt: null,
@@ -824,6 +893,98 @@ async function issueOneTime(context, token) {
824
893
  return { secret, expiresAt };
825
894
  }
826
895
 
896
+ // src/auth/password-written.ts
897
+ async function endSignInsWaiting(context, userId) {
898
+ await context.store.tokens.spendUserTokens(userId, "secondFactor", context.clock.now());
899
+ }
900
+ async function heldByPassword(context, type, verified, password, result, where) {
901
+ const now = await findRecord(context, verified.id, type.name);
902
+ const kept = now !== null && now.password !== null && (now.password.hash === verified.password?.hash || await passwordMatches(context, now, password, where));
903
+ if (kept)
904
+ return true;
905
+ if (result.status === "signedIn") {
906
+ await context.store.sessions.revokeSession(result.session.id, context.clock.now());
907
+ } else {
908
+ await burnOneTime(context, result.challenge, "secondFactor");
909
+ }
910
+ return false;
911
+ }
912
+
913
+ // src/auth/email-flows.ts
914
+ function emailFlows(context, type, at) {
915
+ const { store, clock } = context;
916
+ const redeem = async (kind, secret, where) => {
917
+ const token = await spendOneTime(context, secret, kind, where, "token");
918
+ const user = await findRecord(context, token.userId, type.name);
919
+ if (user === null)
920
+ throw unknownOneTime(where, "token");
921
+ refuseStale(type, user, token, where, "token");
922
+ return { token, user };
923
+ };
924
+ const issue = async (kind, user, where) => {
925
+ const email = emailOf(type, user.fields);
926
+ if (email === null) {
927
+ throw new NotFoundError2(`${where}: the user has no e-mail`, {
928
+ userId: user.id,
929
+ userType: type.name,
930
+ operation: where
931
+ });
932
+ }
933
+ const { secret, expiresAt } = await issueOneTime(context, {
934
+ kind,
935
+ userId: user.id,
936
+ address: email,
937
+ ttlMs: context.config.tokenTtlMs[kind]
938
+ });
939
+ return { token: secret, email, expiresAt };
940
+ };
941
+ return {
942
+ verifyEmail: {
943
+ async send(user) {
944
+ const where = at("verifyEmail.send");
945
+ const record = await getRecord(context, idOf(user), type.name, where);
946
+ return issue("verifyEmail", record, where);
947
+ },
948
+ async confirm(secret) {
949
+ const where = at("verifyEmail.confirm");
950
+ const { token, user } = await redeem("verifyEmail", secret, where);
951
+ return toUser(await writeUser(context, user.id, type, undefined, where, (record, now) => {
952
+ refuseStale(type, record, token, where, "token");
953
+ return { emailVerifiedAt: now };
954
+ }));
955
+ }
956
+ },
957
+ resetPassword: {
958
+ async request(email) {
959
+ const where = at("resetPassword.request");
960
+ passwordRule(type, where);
961
+ const record = await holderOfEmail(context, type, String(email));
962
+ if (record === null)
963
+ return null;
964
+ const issued = await issue("resetPassword", record, where);
965
+ return { ...issued, user: toUser(record) };
966
+ },
967
+ async confirm(secret, password) {
968
+ const where = at("resetPassword.confirm");
969
+ passwordRule(type, where);
970
+ checkPassword(type, password, where);
971
+ const hash = await requireHasher(context, where).hash(password);
972
+ const { token, user } = await redeem("resetPassword", secret, where);
973
+ const written = await writeUser(context, user.id, type, undefined, where, (record, now) => {
974
+ refuseStale(type, record, token, where, "token");
975
+ return {
976
+ password: { hash, updatedAt: now },
977
+ ...record.emailVerifiedAt === null ? { emailVerifiedAt: now } : {}
978
+ };
979
+ });
980
+ await store.sessions.revokeUserSessions(written.id, clock.now());
981
+ await endSignInsWaiting(context, written.id);
982
+ return toUser(written);
983
+ }
984
+ }
985
+ };
986
+ }
987
+
827
988
  // src/auth/totp.ts
828
989
  import { createHmac, randomBytes as randomBytes4 } from "node:crypto";
829
990
  var TOTP = { digits: 6, stepMs: 30000, drift: 1 };
@@ -925,17 +1086,8 @@ function acceptCode(configured, record, factor, code, now, where) {
925
1086
  secret: keyId === configured.sealer.sealWith ? factor.secret : seal(configured.sealer, plain, record.id)
926
1087
  };
927
1088
  }
928
- function codeInvalid(type, where, userId, attemptsLeft) {
929
- return new TokenError2("CODE_INVALID", `${where}: the code does not match, or was already used`, {
930
- operation: where,
931
- userId,
932
- userType: type.name,
933
- ...attemptsLeft === undefined ? {} : { attemptsLeft }
934
- });
935
- }
936
1089
 
937
1090
  // src/auth/second-factor/challenge.ts
938
- var CHALLENGE_ATTEMPTS = 5;
939
1091
  function challengeFlows(context, type, at) {
940
1092
  const { store, clock } = context;
941
1093
  const spend = (challenge, where) => spendOneTime(context, challenge, "secondFactor", where, "challenge");
@@ -948,21 +1100,22 @@ function challengeFlows(context, type, at) {
948
1100
  address: "",
949
1101
  ttlMs: configured.challengeTtlMs
950
1102
  });
951
- return { status: "secondFactor", challenge: secret, expiresAt };
1103
+ return {
1104
+ status: "secondFactor",
1105
+ challenge: secret,
1106
+ expiresAt,
1107
+ userId: record.id
1108
+ };
952
1109
  },
953
1110
  async confirm(challenge, code) {
954
1111
  const where = at("secondFactor.confirm");
955
1112
  const configured = requireSettings(context, where, "a second factor is being confirmed");
956
1113
  const secret = String(challenge);
957
- const token = await store.tokens.countAttempt(hashSecret(secret), "secondFactor");
958
- refuseUnusable(token, clock.now(), where, "challenge");
959
- if (token.attempts > CHALLENGE_ATTEMPTS) {
960
- throw codeInvalid(type, where, token.userId, 0);
961
- }
962
- const attemptsLeft = CHALLENGE_ATTEMPTS - token.attempts;
1114
+ const { token, attemptsLeft } = await countCodeAttempt(context, secret, "secondFactor", where, type.name);
963
1115
  const record = await findRecord(context, token.userId, type.name);
964
- if (record === null)
965
- throw unknownOneTime(where, "challenge");
1116
+ if (record === null) {
1117
+ throw await unknownChallenge(context, token, secret, where);
1118
+ }
966
1119
  if (!record.active) {
967
1120
  await spend(secret, where);
968
1121
  throw new UserInactiveError2(`${where}: the user is inactive`, {
@@ -977,9 +1130,10 @@ function challengeFlows(context, type, at) {
977
1130
  const now = clock.now();
978
1131
  const accepted = acceptCode(configured, record, record.secondFactor, String(code), now, where);
979
1132
  if (accepted === null) {
980
- if (attemptsLeft === 0)
981
- await spend(secret, where);
982
- throw codeInvalid(type, where, record.id, attemptsLeft);
1133
+ if (attemptsLeft === 0) {
1134
+ await burnOneTime(context, secret, "secondFactor");
1135
+ }
1136
+ throw codeInvalid(where, record.id, type.name, attemptsLeft);
983
1137
  }
984
1138
  const written = await store.users.updateUser(record.id, { secondFactor: accepted, updatedAt: now }, record.version);
985
1139
  await spend(secret, where);
@@ -1038,7 +1192,7 @@ function lifecycleFlows(context, type, at) {
1038
1192
  }
1039
1193
  const accepted = acceptCode(configured, record, factor, String(code), now, where);
1040
1194
  if (accepted === null)
1041
- throw codeInvalid(type, where, record.id);
1195
+ throw codeInvalid(where, record.id, type.name);
1042
1196
  return { secondFactor: { ...accepted, confirmedAt: now } };
1043
1197
  });
1044
1198
  return toUser(written);
@@ -1061,8 +1215,57 @@ function secondFactorFlows(context, type, at) {
1061
1215
  };
1062
1216
  return {
1063
1217
  api,
1064
- challenge: challenges.issue,
1065
- required: (record) => isActive(record.secondFactor)
1218
+ finish: (record, where) => isActive(record.secondFactor) ? challenges.issue(record, where) : openSession(context, type, record)
1219
+ };
1220
+ }
1221
+
1222
+ // src/auth/sign-in-code.ts
1223
+ function signInCodeFlows(context, type, at, finish) {
1224
+ return {
1225
+ async request(email) {
1226
+ const record = await holderOfEmail(context, type, String(email));
1227
+ if (record === null || !record.active)
1228
+ return null;
1229
+ const { secret, code, expiresAt } = await issueCode(context, {
1230
+ kind: "signInCode",
1231
+ userId: record.id,
1232
+ address: String(record.fields[type.email]),
1233
+ ttlMs: context.config.tokenTtlMs.signInCode
1234
+ });
1235
+ await context.store.tokens.spendUserTokens(record.id, "signInCode", context.clock.now(), hashSecret(secret));
1236
+ return {
1237
+ code,
1238
+ challenge: secret,
1239
+ email: String(record.fields[type.email]),
1240
+ expiresAt,
1241
+ user: toUser(record)
1242
+ };
1243
+ },
1244
+ async confirm(challenge, code) {
1245
+ const where = at("signInCode.confirm");
1246
+ const secret = String(challenge);
1247
+ const { token, attemptsLeft } = await countCodeAttempt(context, secret, "signInCode", where, type.name);
1248
+ const user = await findRecord(context, token.userId, type.name);
1249
+ if (user === null) {
1250
+ throw await unknownChallenge(context, token, secret, where);
1251
+ }
1252
+ if (!codeMatches(token, secret, String(code))) {
1253
+ if (token.attempts === CODE_ATTEMPTS) {
1254
+ await burnOneTime(context, secret, "signInCode");
1255
+ }
1256
+ throw codeInvalid(where, user.id, type.name, attemptsLeft);
1257
+ }
1258
+ await spendOneTime(context, secret, "signInCode", where, "challenge");
1259
+ refuseStale(type, user, token, where, "code");
1260
+ if (!user.active) {
1261
+ throw new UserInactiveError2(`${where}: the user is inactive`, {
1262
+ userId: user.id,
1263
+ userType: type.name
1264
+ });
1265
+ }
1266
+ const proved = user.emailVerifiedAt === null ? await writeUser(context, user.id, type, { ifVersion: user.version }, where, (_, now) => ({ emailVerifiedAt: now })) : user;
1267
+ return finish(proved, where);
1268
+ }
1066
1269
  };
1067
1270
  }
1068
1271
 
@@ -1071,6 +1274,7 @@ function typeApi(context, type) {
1071
1274
  const { store, clock } = context;
1072
1275
  const at = (operation) => context.config.single ? operation : `${type.name}.${operation}`;
1073
1276
  const secondFactor = secondFactorFlows(context, type, at);
1277
+ const signInCode = signInCodeFlows(context, type, at, secondFactor.finish);
1074
1278
  const byLogin = async (login) => isStorable(login) ? store.users.findUserByLogin(type.name, login) : null;
1075
1279
  const insert = async (input, where) => {
1076
1280
  const { password, active, ...rest } = input ?? {};
@@ -1099,40 +1303,6 @@ function typeApi(context, type) {
1099
1303
  updatedAt: now
1100
1304
  });
1101
1305
  };
1102
- const redeem = async (kind, secret, where) => {
1103
- const token = await spendOneTime(context, secret, kind, where, "token");
1104
- const user = await findRecord(context, token.userId, type.name);
1105
- if (user === null)
1106
- throw unknownOneTime(where, "token");
1107
- const email = emailOf(type, user.fields);
1108
- if (email === null || normalizeEmail(email) !== normalizeEmail(token.address)) {
1109
- throw new TokenError2("TOKEN_STALE", `${where}: the token was sent to an e-mail the user no longer has`, { operation: where, userId: user.id, userType: type.name });
1110
- }
1111
- return { token, user };
1112
- };
1113
- const issue = async (kind, user, where) => {
1114
- const email = emailOf(type, user.fields);
1115
- if (email === null) {
1116
- throw new NotFoundError2(`${where}: the user has no e-mail`, {
1117
- userId: user.id,
1118
- userType: type.name,
1119
- operation: where
1120
- });
1121
- }
1122
- const { secret, expiresAt } = await issueOneTime(context, {
1123
- kind,
1124
- userId: user.id,
1125
- address: email,
1126
- ttlMs: context.config.tokenTtlMs[kind]
1127
- });
1128
- return { token: secret, email, expiresAt };
1129
- };
1130
- const passwordRule = (where) => {
1131
- if (type.password === null) {
1132
- throw new TypeError(`${where}: the ${type.name} type does not sign in with a password — add password: { login } to it`);
1133
- }
1134
- return type.password;
1135
- };
1136
1306
  return {
1137
1307
  async create(input) {
1138
1308
  return toUser(await insert(input, at("create")));
@@ -1192,7 +1362,7 @@ function typeApi(context, type) {
1192
1362
  },
1193
1363
  async signUp(input) {
1194
1364
  const where = at("signUp");
1195
- passwordRule(where);
1365
+ passwordRule(type, where);
1196
1366
  if (input?.password === undefined) {
1197
1367
  checkPassword(type, undefined, where);
1198
1368
  }
@@ -1201,7 +1371,7 @@ function typeApi(context, type) {
1201
1371
  },
1202
1372
  async signIn(input) {
1203
1373
  const where = at("signIn");
1204
- const rule = passwordRule(where);
1374
+ const rule = passwordRule(type, where);
1205
1375
  const hasher = requireHasher(context, where);
1206
1376
  const login = input?.[rule.login];
1207
1377
  const password = input?.password;
@@ -1220,29 +1390,33 @@ function typeApi(context, type) {
1220
1390
  userType: type.name
1221
1391
  });
1222
1392
  }
1223
- const signedIn = await rehashed(context, record, String(password));
1224
- return secondFactor.required(signedIn) ? secondFactor.challenge(signedIn, where) : openSession(context, type, signedIn);
1393
+ const verified = await rehashed(context, record, String(password));
1394
+ const result = await secondFactor.finish(verified, where);
1395
+ if (!await heldByPassword(context, type, verified, String(password), result, where)) {
1396
+ throw refuse("wrongPassword");
1397
+ }
1398
+ return result;
1225
1399
  },
1226
1400
  async findByLogin(login) {
1227
- const rule = passwordRule(at("findByLogin"));
1401
+ const rule = passwordRule(type, at("findByLogin"));
1228
1402
  const record = await byLogin(rule.normalize(login));
1229
1403
  return record === null ? null : toUser(record);
1230
1404
  },
1231
1405
  async setPassword(user, password, options) {
1232
1406
  const where = at("setPassword");
1233
- passwordRule(where);
1407
+ passwordRule(type, where);
1234
1408
  checkPassword(type, password, where);
1235
1409
  const hash = await requireHasher(context, where).hash(password);
1236
- return toUser(await writeUser(context, user, type, options, where, (_, now) => ({
1237
- password: { hash, updatedAt: now }
1238
- })));
1410
+ const written = await writeUser(context, user, type, options, where, (_, now) => ({ password: { hash, updatedAt: now } }));
1411
+ await endSignInsWaiting(context, written.id);
1412
+ return toUser(written);
1239
1413
  },
1240
1414
  async changePassword(user, change, options) {
1241
1415
  const where = at("changePassword");
1242
- passwordRule(where);
1416
+ passwordRule(type, where);
1243
1417
  checkPassword(type, change?.next, where);
1244
1418
  const hasher = requireHasher(context, where);
1245
- return toUser(await writeUser(context, user, type, options, where, async (record, now) => {
1419
+ const written = await writeUser(context, user, type, options, where, async (record, now) => {
1246
1420
  if (record.password === null || !await passwordMatches(context, record, change.current, where)) {
1247
1421
  throw new CredentialError2("CREDENTIALS_INVALID", `${where}: the current password does not match`, {
1248
1422
  reason: record.password === null ? "noPassword" : "wrongPassword",
@@ -1256,50 +1430,13 @@ function typeApi(context, type) {
1256
1430
  updatedAt: now
1257
1431
  }
1258
1432
  };
1259
- }));
1433
+ });
1434
+ await endSignInsWaiting(context, written.id);
1435
+ return toUser(written);
1260
1436
  },
1261
1437
  secondFactor: secondFactor.api,
1262
- verifyEmail: {
1263
- async send(user) {
1264
- const where = at("verifyEmail.send");
1265
- const record = await getRecord(context, idOf(user), type.name, where);
1266
- return issue("verifyEmail", record, where);
1267
- },
1268
- async confirm(secret) {
1269
- const where = at("verifyEmail.confirm");
1270
- const { user } = await redeem("verifyEmail", secret, where);
1271
- return toUser(await writeUser(context, user.id, type, undefined, where, (_, now) => ({
1272
- emailVerifiedAt: now
1273
- })));
1274
- }
1275
- },
1276
- resetPassword: {
1277
- async request(email) {
1278
- const where = at("resetPassword.request");
1279
- passwordRule(where);
1280
- const wanted = normalizeEmail(String(email));
1281
- const record = await byLogin(wanted);
1282
- const held = record === null ? null : emailOf(type, record.fields);
1283
- if (record === null || held === null || normalizeEmail(held) !== wanted) {
1284
- return null;
1285
- }
1286
- const issued = await issue("resetPassword", record, where);
1287
- return { ...issued, user: toUser(record) };
1288
- },
1289
- async confirm(secret, password) {
1290
- const where = at("resetPassword.confirm");
1291
- passwordRule(where);
1292
- checkPassword(type, password, where);
1293
- const hash = await requireHasher(context, where).hash(password);
1294
- const { user } = await redeem("resetPassword", secret, where);
1295
- const written = await writeUser(context, user.id, type, undefined, where, (record, now) => ({
1296
- password: { hash, updatedAt: now },
1297
- ...record.emailVerifiedAt === null ? { emailVerifiedAt: now } : {}
1298
- }));
1299
- await store.sessions.revokeUserSessions(written.id, clock.now());
1300
- return toUser(written);
1301
- }
1302
- }
1438
+ signInCode,
1439
+ ...emailFlows(context, type, at)
1303
1440
  };
1304
1441
  }
1305
1442
 
@@ -1375,5 +1512,5 @@ export {
1375
1512
  systemClock
1376
1513
  };
1377
1514
 
1378
- //# debugId=FA6925B96B6EEFD664756E2164756E21
1515
+ //# debugId=8A3012761E63D76664756E2164756E21
1379
1516
  //# sourceMappingURL=index.js.map