@nxgt/janus 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +102 -18
  2. package/dist/auth/config.d.ts +25 -1
  3. package/dist/auth/config.d.ts.map +1 -1
  4. package/dist/auth/context.d.ts.map +1 -1
  5. package/dist/auth/index.d.ts +3 -2
  6. package/dist/auth/index.d.ts.map +1 -1
  7. package/dist/auth/one-time.d.ts +39 -0
  8. package/dist/auth/one-time.d.ts.map +1 -0
  9. package/dist/auth/port/types.d.ts +10 -7
  10. package/dist/auth/port/types.d.ts.map +1 -1
  11. package/dist/auth/sealing.d.ts +39 -0
  12. package/dist/auth/sealing.d.ts.map +1 -0
  13. package/dist/auth/second-factor/challenge.d.ts +20 -0
  14. package/dist/auth/second-factor/challenge.d.ts.map +1 -0
  15. package/dist/auth/second-factor/factor.d.ts +30 -0
  16. package/dist/auth/second-factor/factor.d.ts.map +1 -0
  17. package/dist/auth/second-factor/flows.d.ts +19 -0
  18. package/dist/auth/second-factor/flows.d.ts.map +1 -0
  19. package/dist/auth/second-factor/lifecycle.d.ts +8 -0
  20. package/dist/auth/second-factor/lifecycle.d.ts.map +1 -0
  21. package/dist/auth/sessions.d.ts.map +1 -1
  22. package/dist/auth/totp.d.ts +36 -0
  23. package/dist/auth/totp.d.ts.map +1 -0
  24. package/dist/auth/types.d.ts +91 -7
  25. package/dist/auth/types.d.ts.map +1 -1
  26. package/dist/auth/users.d.ts +2 -2
  27. package/dist/auth/users.d.ts.map +1 -1
  28. package/dist/chunks/{index-qwfkhqkk.js → index-06vp9c5r.js} +12 -3
  29. package/dist/chunks/{index-qwfkhqkk.js.map → index-06vp9c5r.js.map} +3 -3
  30. package/dist/chunks/{index-53y1afjz.js → index-5vr13kkb.js} +2 -2
  31. package/dist/chunks/{index-mgh85djb.js → index-c4v27jfr.js} +2 -2
  32. package/dist/chunks/{index-thtyq7a9.js → index-gbwn2tts.js} +2 -2
  33. package/dist/conformance/cases/tokens.d.ts.map +1 -1
  34. package/dist/conformance/index.js +15 -4
  35. package/dist/conformance/index.js.map +3 -3
  36. package/dist/errors/janus-error.d.ts +28 -2
  37. package/dist/errors/janus-error.d.ts.map +1 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +388 -41
  41. package/dist/index.js.map +14 -7
  42. package/dist/permissions/index.js +3 -3
  43. package/docs/README.md +1 -0
  44. package/docs/guide/adapters.md +12 -7
  45. package/docs/guide/errors.md +18 -6
  46. package/docs/guide/second-factor.md +569 -0
  47. package/docs/guide/sessions.md +18 -0
  48. package/docs/guide/users.md +8 -2
  49. package/docs/guide/vocabulary.md +6 -3
  50. package/docs/roadmap.md +26 -48
  51. package/docs/troubleshooting.md +260 -5
  52. package/package.json +1 -1
  53. /package/dist/chunks/{index-53y1afjz.js.map → index-5vr13kkb.js.map} +0 -0
  54. /package/dist/chunks/{index-mgh85djb.js.map → index-c4v27jfr.js.map} +0 -0
  55. /package/dist/chunks/{index-thtyq7a9.js.map → index-gbwn2tts.js.map} +0 -0
package/dist/index.js CHANGED
@@ -5,13 +5,13 @@ import {
5
5
  MAX_PAGE_SIZE2,
6
6
  pageLimit2,
7
7
  invalidCursor2
8
- } from "./chunks/index-53y1afjz.js";
8
+ } from "./chunks/index-5vr13kkb.js";
9
9
  import {
10
10
  mintId2,
11
11
  isId2,
12
12
  mintedAt2,
13
13
  createMemoryStores2
14
- } from "./chunks/index-thtyq7a9.js";
14
+ } from "./chunks/index-gbwn2tts.js";
15
15
  import {
16
16
  JanusError2,
17
17
  StoreFailure2,
@@ -21,6 +21,7 @@ import {
21
21
  CredentialError2,
22
22
  UserInactiveError2,
23
23
  TokenError2,
24
+ SecondFactorError2,
24
25
  InvalidCursorError2,
25
26
  UnsupportedError2,
26
27
  PermissionDepthError2,
@@ -33,7 +34,7 @@ import {
33
34
  formatTuple2,
34
35
  parseTuple2,
35
36
  parseSubject2
36
- } from "./chunks/index-qwfkhqkk.js";
37
+ } from "./chunks/index-06vp9c5r.js";
37
38
 
38
39
  // src/auth/hashers.ts
39
40
  import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
@@ -140,6 +141,69 @@ function parseDuration(value, where) {
140
141
  return ms;
141
142
  }
142
143
 
144
+ // src/auth/sealing.ts
145
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes2 } from "node:crypto";
146
+ var TAG_BYTES = 16;
147
+ var KEY_ID = /^[A-Za-z0-9_-]{1,64}$/;
148
+ var KEY = /^[A-Za-z0-9+/_-]{43}=?$/;
149
+ function resolveSealer(keys, where) {
150
+ if (!Array.isArray(keys) || keys.length === 0) {
151
+ throw new TypeError(`${where}: expected at least one key — [{ id, key }], the first seals`);
152
+ }
153
+ const decoded = new Map;
154
+ for (const entry of keys) {
155
+ const id = entry?.id;
156
+ if (typeof id !== "string" || !KEY_ID.test(id)) {
157
+ throw new TypeError(`${where}: every key needs an id of letters, digits, _ and -, at most 64 of them`);
158
+ }
159
+ if (decoded.has(id)) {
160
+ throw new TypeError(`${where}: two keys have the id "${id}"`);
161
+ }
162
+ const key = entry.key;
163
+ if (typeof key !== "string" || !KEY.test(key)) {
164
+ throw new TypeError(`${where}: the key "${id}" is not 32 bytes in base64 — make one with openssl rand -base64 32`);
165
+ }
166
+ decoded.set(id, Buffer.from(key, "base64"));
167
+ }
168
+ return { sealWith: keys[0].id, keys: decoded };
169
+ }
170
+ function seal(sealer, plain, boundTo) {
171
+ const key = sealer.keys.get(sealer.sealWith);
172
+ const iv = randomBytes2(12);
173
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
174
+ cipher.setAAD(Buffer.from(boundTo));
175
+ const sealed = Buffer.concat([
176
+ cipher.update(plain, "utf8"),
177
+ cipher.final(),
178
+ cipher.getAuthTag()
179
+ ]);
180
+ return `v1.${sealer.sealWith}.${iv.toString("base64url")}.${sealed.toString("base64url")}`;
181
+ }
182
+ function unseal(sealer, sealed, boundTo, where) {
183
+ const parts = sealed.split(".");
184
+ const [version, keyId, iv, body] = parts;
185
+ const bytes = Buffer.from(body ?? "", "base64url");
186
+ if (parts.length !== 4 || version !== "v1" || keyId === undefined || iv === undefined || bytes.length <= TAG_BYTES) {
187
+ throw new TypeError(`${where}: the stored secret is not a sealed one`);
188
+ }
189
+ const key = sealer.keys.get(keyId);
190
+ if (key === undefined) {
191
+ throw new TypeError(`${where}: the secret is sealed with the key "${keyId}", which secondFactor.keys no longer holds — keep a key until no secret is sealed with it`);
192
+ }
193
+ try {
194
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "base64url"), { authTagLength: TAG_BYTES });
195
+ decipher.setAAD(Buffer.from(boundTo));
196
+ decipher.setAuthTag(bytes.subarray(bytes.length - TAG_BYTES));
197
+ const plain = Buffer.concat([
198
+ decipher.update(bytes.subarray(0, bytes.length - TAG_BYTES)),
199
+ decipher.final()
200
+ ]).toString("utf8");
201
+ return { plain, keyId };
202
+ } catch (cause) {
203
+ throw new TypeError(`${where}: the secret does not open with the key "${keyId}" — was that key changed under the same id, or the secret copied from another user?`, { cause });
204
+ }
205
+ }
206
+
143
207
  // src/auth/config.ts
144
208
  var SINGLE_TYPE = "user";
145
209
  var RESERVED_FIELDS = [
@@ -148,6 +212,7 @@ var RESERVED_FIELDS = [
148
212
  "emailVerified",
149
213
  "active",
150
214
  "hasPassword",
215
+ "hasSecondFactor",
151
216
  "version",
152
217
  "createdAt",
153
218
  "updatedAt",
@@ -215,7 +280,21 @@ function resolveConfig(config, where) {
215
280
  verifyEmail: parseDuration(config.tokens?.verifyEmail ?? "24h", `${where}: tokens.verifyEmail`),
216
281
  resetPassword: parseDuration(config.tokens?.resetPassword ?? "1h", `${where}: tokens.resetPassword`)
217
282
  },
218
- cookie: resolveCookie(config.cookie ?? {}, where)
283
+ cookie: resolveCookie(config.cookie ?? {}, where),
284
+ secondFactor: resolveSecondFactor(config.secondFactor, where)
285
+ };
286
+ }
287
+ function resolveSecondFactor(config, where) {
288
+ if (config === undefined)
289
+ return null;
290
+ const at = `${where}: secondFactor`;
291
+ if (typeof config?.issuer !== "string" || config.issuer.trim() === "") {
292
+ throw new TypeError(`${at}.issuer must name your application — the authenticator app shows it beside the account`);
293
+ }
294
+ return {
295
+ issuer: config.issuer,
296
+ sealer: resolveSealer(config.keys, `${at}.keys`),
297
+ challengeTtlMs: parseDuration(config.challenge ?? "5m", `${at}.challenge`)
219
298
  };
220
299
  }
221
300
  function resolveType(name, type, at, schemaKey) {
@@ -336,6 +415,7 @@ function toUser(record) {
336
415
  emailVerified: record.emailVerifiedAt !== null,
337
416
  active: record.active,
338
417
  hasPassword: record.password !== null,
418
+ hasSecondFactor: record.secondFactor?.confirmedAt != null,
339
419
  version: record.version,
340
420
  createdAt: record.createdAt,
341
421
  updatedAt: record.updatedAt
@@ -544,9 +624,9 @@ function assertStores(stores, where) {
544
624
  }
545
625
 
546
626
  // src/auth/secrets.ts
547
- import { createHash, randomBytes as randomBytes2 } from "node:crypto";
627
+ import { createHash, randomBytes as randomBytes3 } from "node:crypto";
548
628
  function mintSecret() {
549
- return randomBytes2(32).toString("base64url");
629
+ return randomBytes3(32).toString("base64url");
550
630
  }
551
631
  function hashSecret(secret) {
552
632
  return createHash("sha256").update(secret).digest("hex");
@@ -570,7 +650,12 @@ async function openSession(context, type, user) {
570
650
  createdAt: now
571
651
  };
572
652
  await context.store.sessions.insertSession(record);
573
- return { user: toUser(user), session: toSession(record), token };
653
+ return {
654
+ status: "signedIn",
655
+ user: toUser(user),
656
+ session: toSession(record),
657
+ token
658
+ };
574
659
  }
575
660
  function sharedApi(context) {
576
661
  const { store, clock, config } = context;
@@ -696,10 +781,296 @@ function headersOf(request) {
696
781
  return request;
697
782
  }
698
783
 
784
+ // src/auth/one-time.ts
785
+ function refuseUnusable(token, now, where, noun) {
786
+ if (token === null) {
787
+ throw new TokenError2("TOKEN_UNKNOWN", `${where}: no such ${noun}`, {
788
+ operation: where
789
+ });
790
+ }
791
+ if (token.spentAt !== null) {
792
+ throw new TokenError2("TOKEN_SPENT", `${where}: the ${noun} was already used`, { operation: where });
793
+ }
794
+ if (token.expiresAt.getTime() <= now.getTime()) {
795
+ throw new TokenError2("TOKEN_EXPIRED", `${where}: the ${noun} has expired`, {
796
+ operation: where
797
+ });
798
+ }
799
+ }
800
+ var unknownOneTime = (where, noun) => new TokenError2("TOKEN_UNKNOWN", `${where}: no such ${noun}`, {
801
+ operation: where
802
+ });
803
+ async function spendOneTime(context, secret, kind, where, noun) {
804
+ const now = context.clock.now();
805
+ const token = await context.store.tokens.consumeToken(hashSecret(secret), kind, now);
806
+ refuseUnusable(token, now, where, noun);
807
+ return token;
808
+ }
809
+ async function issueOneTime(context, token) {
810
+ const now = context.clock.now();
811
+ const secret = mintSecret();
812
+ const expiresAt = new Date(now.getTime() + token.ttlMs);
813
+ await context.store.tokens.insertToken({
814
+ tokenHash: hashSecret(secret),
815
+ kind: token.kind,
816
+ userId: token.userId,
817
+ address: token.address,
818
+ codeHash: null,
819
+ attempts: 0,
820
+ expiresAt,
821
+ spentAt: null,
822
+ createdAt: now
823
+ });
824
+ return { secret, expiresAt };
825
+ }
826
+
827
+ // src/auth/totp.ts
828
+ import { createHmac, randomBytes as randomBytes4 } from "node:crypto";
829
+ var TOTP = { digits: 6, stepMs: 30000, drift: 1 };
830
+ var BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
831
+ function mintTotpSecret() {
832
+ return toBase32(randomBytes4(20));
833
+ }
834
+ function toBase32(bytes) {
835
+ let bits = 0;
836
+ let value = 0;
837
+ let out = "";
838
+ for (const byte of bytes) {
839
+ value = value << 8 | byte;
840
+ bits += 8;
841
+ while (bits >= 5) {
842
+ out += BASE32[value >>> bits - 5 & 31];
843
+ bits -= 5;
844
+ }
845
+ }
846
+ if (bits > 0)
847
+ out += BASE32[value << 5 - bits & 31];
848
+ return out;
849
+ }
850
+ function fromBase32(text) {
851
+ let bits = 0;
852
+ let value = 0;
853
+ const out = [];
854
+ for (const char of text) {
855
+ const index = BASE32.indexOf(char);
856
+ if (index === -1)
857
+ throw new TypeError("totp: the secret is not base32");
858
+ value = value << 5 | index;
859
+ bits += 5;
860
+ if (bits >= 8) {
861
+ out.push(value >>> bits - 8 & 255);
862
+ bits -= 8;
863
+ }
864
+ }
865
+ return Buffer.from(out);
866
+ }
867
+ var stepAt = (now) => Math.floor(now.getTime() / TOTP.stepMs);
868
+ function codeAt(secret, step) {
869
+ const counter = Buffer.alloc(8);
870
+ counter.writeBigUInt64BE(BigInt(step));
871
+ const digest = createHmac("sha1", secret).update(counter).digest();
872
+ const offset = digest[19] & 15;
873
+ const binary = digest.readUInt32BE(offset) & 2147483647;
874
+ return String(binary % 10 ** TOTP.digits).padStart(TOTP.digits, "0");
875
+ }
876
+ function matchStep(secret, code, now, lastStep) {
877
+ if (!/^\d{6}$/.test(code))
878
+ return null;
879
+ const current = stepAt(now);
880
+ let matched = null;
881
+ for (let step = current - TOTP.drift;step <= current + TOTP.drift; step++) {
882
+ const fresh = lastStep === null || step > lastStep;
883
+ if (equalCodes(codeAt(secret, step), code) && fresh && matched === null) {
884
+ matched = step;
885
+ }
886
+ }
887
+ return matched;
888
+ }
889
+ function equalCodes(a, b) {
890
+ let difference = a.length ^ b.length;
891
+ for (let i = 0;i < a.length; i++) {
892
+ difference |= a.charCodeAt(i) ^ b.charCodeAt(i);
893
+ }
894
+ return difference === 0;
895
+ }
896
+ function otpauthUri(issuer, account, secret) {
897
+ const label = `${encodeURIComponent(issuer)}:${encodeURIComponent(account)}`;
898
+ const query = new URLSearchParams({
899
+ secret,
900
+ issuer,
901
+ algorithm: "SHA1",
902
+ digits: String(TOTP.digits),
903
+ period: String(TOTP.stepMs / 1000)
904
+ });
905
+ return `otpauth://totp/${label}?${query}`;
906
+ }
907
+
908
+ // src/auth/second-factor/factor.ts
909
+ var isActive = (factor) => factor !== null && factor.confirmedAt !== null;
910
+ function requireSettings(context, where, why) {
911
+ const configured = context.config.secondFactor;
912
+ if (configured === null) {
913
+ throw new TypeError(`${where}: ${why}, and janus() was given no secondFactor — pass secondFactor: { issuer, keys }`);
914
+ }
915
+ return configured;
916
+ }
917
+ function acceptCode(configured, record, factor, code, now, where) {
918
+ const { plain, keyId } = unseal(configured.sealer, factor.secret, record.id, where);
919
+ const step = matchStep(fromBase32(plain), code, now, factor.lastStep);
920
+ if (step === null)
921
+ return null;
922
+ return {
923
+ ...factor,
924
+ lastStep: step,
925
+ secret: keyId === configured.sealer.sealWith ? factor.secret : seal(configured.sealer, plain, record.id)
926
+ };
927
+ }
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
+
937
+ // src/auth/second-factor/challenge.ts
938
+ var CHALLENGE_ATTEMPTS = 5;
939
+ function challengeFlows(context, type, at) {
940
+ const { store, clock } = context;
941
+ const spend = (challenge, where) => spendOneTime(context, challenge, "secondFactor", where, "challenge");
942
+ return {
943
+ async issue(record, where) {
944
+ const configured = requireSettings(context, where, "the user's second factor is active");
945
+ const { secret, expiresAt } = await issueOneTime(context, {
946
+ kind: "secondFactor",
947
+ userId: record.id,
948
+ address: "",
949
+ ttlMs: configured.challengeTtlMs
950
+ });
951
+ return { status: "secondFactor", challenge: secret, expiresAt };
952
+ },
953
+ async confirm(challenge, code) {
954
+ const where = at("secondFactor.confirm");
955
+ const configured = requireSettings(context, where, "a second factor is being confirmed");
956
+ 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;
963
+ const record = await findRecord(context, token.userId, type.name);
964
+ if (record === null)
965
+ throw unknownOneTime(where, "challenge");
966
+ if (!record.active) {
967
+ await spend(secret, where);
968
+ throw new UserInactiveError2(`${where}: the user is inactive`, {
969
+ userId: record.id,
970
+ userType: type.name
971
+ });
972
+ }
973
+ if (!isActive(record.secondFactor)) {
974
+ await spend(secret, where);
975
+ throw new SecondFactorError2("SECOND_FACTOR_NOT_ENROLLED", `${where}: the user no longer has a second factor — sign in again`, { operation: where, userId: record.id, userType: type.name });
976
+ }
977
+ const now = clock.now();
978
+ const accepted = acceptCode(configured, record, record.secondFactor, String(code), now, where);
979
+ if (accepted === null) {
980
+ if (attemptsLeft === 0)
981
+ await spend(secret, where);
982
+ throw codeInvalid(type, where, record.id, attemptsLeft);
983
+ }
984
+ const written = await store.users.updateUser(record.id, { secondFactor: accepted, updatedAt: now }, record.version);
985
+ await spend(secret, where);
986
+ return openSession(context, type, written);
987
+ }
988
+ };
989
+ }
990
+
991
+ // src/auth/second-factor/lifecycle.ts
992
+ function lifecycleFlows(context, type, at) {
993
+ const accountOf = (record, where) => {
994
+ if (type.password === null) {
995
+ throw new TypeError(`${where}: the ${type.name} type does not sign in with a password, so it has no second factor`);
996
+ }
997
+ return String(record.fields[type.password.login]);
998
+ };
999
+ const refuse = (code, message, where, record) => new SecondFactorError2(code, `${where}: ${message}`, {
1000
+ operation: where,
1001
+ userId: record.id,
1002
+ userType: type.name
1003
+ });
1004
+ return {
1005
+ async enroll(user, options) {
1006
+ const where = at("secondFactor.enroll");
1007
+ const configured = requireSettings(context, where, "a second factor is being enrolled");
1008
+ const secret = mintTotpSecret();
1009
+ const written = await writeUser(context, user, type, options, where, (record) => {
1010
+ if (isActive(record.secondFactor)) {
1011
+ throw refuse("SECOND_FACTOR_ACTIVE", "the user's second factor is active — disable it first", where, record);
1012
+ }
1013
+ accountOf(record, where);
1014
+ return {
1015
+ secondFactor: {
1016
+ method: "totp",
1017
+ secret: seal(configured.sealer, secret, record.id),
1018
+ confirmedAt: null,
1019
+ lastStep: null
1020
+ }
1021
+ };
1022
+ });
1023
+ return {
1024
+ secret,
1025
+ uri: otpauthUri(configured.issuer, accountOf(written, where), secret)
1026
+ };
1027
+ },
1028
+ async activate(user, code, options) {
1029
+ const where = at("secondFactor.activate");
1030
+ const configured = requireSettings(context, where, "a second factor is being activated");
1031
+ const written = await writeUser(context, user, type, options, where, (record, now) => {
1032
+ const factor = record.secondFactor;
1033
+ if (factor === null) {
1034
+ throw refuse("SECOND_FACTOR_NOT_ENROLLED", "the user has no second factor waiting — call enroll first", where, record);
1035
+ }
1036
+ if (isActive(factor)) {
1037
+ throw refuse("SECOND_FACTOR_ACTIVE", "the user's second factor is already active", where, record);
1038
+ }
1039
+ const accepted = acceptCode(configured, record, factor, String(code), now, where);
1040
+ if (accepted === null)
1041
+ throw codeInvalid(type, where, record.id);
1042
+ return { secondFactor: { ...accepted, confirmedAt: now } };
1043
+ });
1044
+ return toUser(written);
1045
+ },
1046
+ async disable(user, options) {
1047
+ const where = at("secondFactor.disable");
1048
+ return toUser(await writeUser(context, user, type, options, where, () => ({
1049
+ secondFactor: null
1050
+ })));
1051
+ }
1052
+ };
1053
+ }
1054
+
1055
+ // src/auth/second-factor/flows.ts
1056
+ function secondFactorFlows(context, type, at) {
1057
+ const challenges = challengeFlows(context, type, at);
1058
+ const api = {
1059
+ ...lifecycleFlows(context, type, at),
1060
+ confirm: challenges.confirm
1061
+ };
1062
+ return {
1063
+ api,
1064
+ challenge: challenges.issue,
1065
+ required: (record) => isActive(record.secondFactor)
1066
+ };
1067
+ }
1068
+
699
1069
  // src/auth/users.ts
700
1070
  function typeApi(context, type) {
701
1071
  const { store, clock } = context;
702
1072
  const at = (operation) => context.config.single ? operation : `${type.name}.${operation}`;
1073
+ const secondFactor = secondFactorFlows(context, type, at);
703
1074
  const byLogin = async (login) => isStorable(login) ? store.users.findUserByLogin(type.name, login) : null;
704
1075
  const insert = async (input, where) => {
705
1076
  const { password, active, ...rest } = input ?? {};
@@ -729,29 +1100,10 @@ function typeApi(context, type) {
729
1100
  });
730
1101
  };
731
1102
  const redeem = async (kind, secret, where) => {
732
- const now = clock.now();
733
- const token = await store.tokens.consumeToken(hashSecret(secret), kind, now);
734
- if (token === null) {
735
- throw new TokenError2("TOKEN_UNKNOWN", `${where}: no such token`, {
736
- operation: where
737
- });
738
- }
739
- if (token.spentAt !== null) {
740
- throw new TokenError2("TOKEN_SPENT", `${where}: the token was already used`, {
741
- operation: where
742
- });
743
- }
744
- if (token.expiresAt.getTime() <= now.getTime()) {
745
- throw new TokenError2("TOKEN_EXPIRED", `${where}: the token has expired`, {
746
- operation: where
747
- });
748
- }
1103
+ const token = await spendOneTime(context, secret, kind, where, "token");
749
1104
  const user = await findRecord(context, token.userId, type.name);
750
- if (user === null) {
751
- throw new TokenError2("TOKEN_UNKNOWN", `${where}: no such token`, {
752
- operation: where
753
- });
754
- }
1105
+ if (user === null)
1106
+ throw unknownOneTime(where, "token");
755
1107
  const email = emailOf(type, user.fields);
756
1108
  if (email === null || normalizeEmail(email) !== normalizeEmail(token.address)) {
757
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 });
@@ -767,19 +1119,11 @@ function typeApi(context, type) {
767
1119
  operation: where
768
1120
  });
769
1121
  }
770
- const now = clock.now();
771
- const secret = mintSecret();
772
- const expiresAt = new Date(now.getTime() + context.config.tokenTtlMs[kind]);
773
- await store.tokens.insertToken({
774
- tokenHash: hashSecret(secret),
1122
+ const { secret, expiresAt } = await issueOneTime(context, {
775
1123
  kind,
776
1124
  userId: user.id,
777
1125
  address: email,
778
- codeHash: null,
779
- attempts: 0,
780
- expiresAt,
781
- spentAt: null,
782
- createdAt: now
1126
+ ttlMs: context.config.tokenTtlMs[kind]
783
1127
  });
784
1128
  return { token: secret, email, expiresAt };
785
1129
  };
@@ -876,7 +1220,8 @@ function typeApi(context, type) {
876
1220
  userType: type.name
877
1221
  });
878
1222
  }
879
- return openSession(context, type, await rehashed(context, record, String(password)));
1223
+ const signedIn = await rehashed(context, record, String(password));
1224
+ return secondFactor.required(signedIn) ? secondFactor.challenge(signedIn, where) : openSession(context, type, signedIn);
880
1225
  },
881
1226
  async findByLogin(login) {
882
1227
  const rule = passwordRule(at("findByLogin"));
@@ -913,6 +1258,7 @@ function typeApi(context, type) {
913
1258
  };
914
1259
  }));
915
1260
  },
1261
+ secondFactor: secondFactor.api,
916
1262
  verifyEmail: {
917
1263
  async send(user) {
918
1264
  const where = at("verifyEmail.send");
@@ -998,6 +1344,7 @@ export {
998
1344
  MAX_PAGE_SIZE2 as MAX_PAGE_SIZE,
999
1345
  NotFoundError2 as NotFoundError,
1000
1346
  PermissionDepthError2 as PermissionDepthError,
1347
+ SecondFactorError2 as SecondFactorError,
1001
1348
  StoreConflict2 as StoreConflict,
1002
1349
  StoreFailure2 as StoreFailure,
1003
1350
  TokenError2 as TokenError,
@@ -1028,5 +1375,5 @@ export {
1028
1375
  systemClock
1029
1376
  };
1030
1377
 
1031
- //# debugId=0C2151B9E5FBCB0864756E2164756E21
1378
+ //# debugId=FA6925B96B6EEFD664756E2164756E21
1032
1379
  //# sourceMappingURL=index.js.map