@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.
- package/README.md +116 -23
- package/dist/auth/config.d.ts +3 -0
- package/dist/auth/config.d.ts.map +1 -1
- package/dist/auth/context.d.ts +12 -0
- package/dist/auth/context.d.ts.map +1 -1
- package/dist/auth/email-flows.d.ts +11 -0
- package/dist/auth/email-flows.d.ts.map +1 -0
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/one-time.d.ts +66 -6
- package/dist/auth/one-time.d.ts.map +1 -1
- package/dist/auth/password-written.d.ts +24 -0
- package/dist/auth/password-written.d.ts.map +1 -0
- package/dist/auth/port/assert-stores.d.ts.map +1 -1
- package/dist/auth/port/types.d.ts +23 -1
- package/dist/auth/port/types.d.ts.map +1 -1
- package/dist/auth/second-factor/challenge.d.ts +0 -6
- package/dist/auth/second-factor/challenge.d.ts.map +1 -1
- package/dist/auth/second-factor/factor.d.ts +1 -4
- package/dist/auth/second-factor/factor.d.ts.map +1 -1
- package/dist/auth/second-factor/flows.d.ts +7 -3
- package/dist/auth/second-factor/flows.d.ts.map +1 -1
- package/dist/auth/second-factor/lifecycle.d.ts.map +1 -1
- package/dist/auth/sign-in-code.d.ts +20 -0
- package/dist/auth/sign-in-code.d.ts.map +1 -0
- package/dist/auth/types.d.ts +56 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth/users.d.ts +2 -2
- package/dist/auth/users.d.ts.map +1 -1
- package/dist/chunks/{index-gbwn2tts.js → index-c0jajeda.js} +12 -2
- package/dist/chunks/{index-gbwn2tts.js.map → index-c0jajeda.js.map} +3 -3
- package/dist/conformance/cases/outage.d.ts.map +1 -1
- package/dist/conformance/cases/tokens.d.ts.map +1 -1
- package/dist/conformance/index.js +73 -2
- package/dist/conformance/index.js.map +4 -4
- package/dist/index.js +253 -116
- package/dist/index.js.map +15 -12
- package/docs/README.md +2 -1
- package/docs/guide/adapters.md +56 -3
- package/docs/guide/email-flows.md +11 -1
- package/docs/guide/errors.md +3 -3
- package/docs/guide/second-factor.md +31 -4
- package/docs/guide/sign-in-code.md +489 -0
- package/docs/guide/users.md +5 -4
- package/docs/guide/vocabulary.md +7 -6
- package/docs/roadmap.md +28 -13
- package/docs/troubleshooting.md +193 -28
- 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-
|
|
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: [
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
982
|
-
|
|
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(
|
|
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
|
-
|
|
1065
|
-
|
|
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
|
|
1224
|
-
|
|
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
|
-
|
|
1237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1263
|
-
|
|
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=
|
|
1515
|
+
//# debugId=8A3012761E63D76664756E2164756E21
|
|
1379
1516
|
//# sourceMappingURL=index.js.map
|