@nxgt/janus 0.6.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 (36) hide show
  1. package/README.md +52 -14
  2. package/dist/auth/context.d.ts +6 -0
  3. package/dist/auth/context.d.ts.map +1 -1
  4. package/dist/auth/email-flows.d.ts +11 -0
  5. package/dist/auth/email-flows.d.ts.map +1 -0
  6. package/dist/auth/one-time.d.ts +7 -0
  7. package/dist/auth/one-time.d.ts.map +1 -1
  8. package/dist/auth/password-written.d.ts +24 -0
  9. package/dist/auth/password-written.d.ts.map +1 -0
  10. package/dist/auth/port/assert-stores.d.ts.map +1 -1
  11. package/dist/auth/port/types.d.ts +23 -1
  12. package/dist/auth/port/types.d.ts.map +1 -1
  13. package/dist/auth/second-factor/challenge.d.ts.map +1 -1
  14. package/dist/auth/sign-in-code.d.ts +6 -3
  15. package/dist/auth/sign-in-code.d.ts.map +1 -1
  16. package/dist/auth/types.d.ts +6 -0
  17. package/dist/auth/types.d.ts.map +1 -1
  18. package/dist/auth/users.d.ts.map +1 -1
  19. package/dist/chunks/{index-gbwn2tts.js → index-c0jajeda.js} +12 -2
  20. package/dist/chunks/{index-gbwn2tts.js.map → index-c0jajeda.js.map} +3 -3
  21. package/dist/conformance/cases/outage.d.ts.map +1 -1
  22. package/dist/conformance/cases/tokens.d.ts.map +1 -1
  23. package/dist/conformance/index.js +73 -2
  24. package/dist/conformance/index.js.map +4 -4
  25. package/dist/index.js +145 -88
  26. package/dist/index.js.map +11 -9
  27. package/docs/README.md +1 -1
  28. package/docs/guide/adapters.md +56 -3
  29. package/docs/guide/email-flows.md +7 -1
  30. package/docs/guide/second-factor.md +27 -2
  31. package/docs/guide/sign-in-code.md +26 -8
  32. package/docs/guide/users.md +3 -3
  33. package/docs/guide/vocabulary.md +1 -0
  34. package/docs/roadmap.md +12 -4
  35. package/docs/troubleshooting.md +37 -26
  36. 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,
@@ -514,6 +514,12 @@ function loginsOf(type, fields, where) {
514
514
  logins.add(normalizeEmail(email));
515
515
  return [...logins];
516
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
+ }
517
523
  function checkPassword(type, password, where) {
518
524
  const minLength = type.password?.minLength ?? 8;
519
525
  if (typeof password !== "string" || password.length < minLength) {
@@ -607,7 +613,13 @@ var REQUIRED = {
607
613
  "revokeUserSessions",
608
614
  "deleteUserSessions"
609
615
  ],
610
- tokens: ["insertToken", "consumeToken", "countAttempt", "deleteUserTokens"]
616
+ tokens: [
617
+ "insertToken",
618
+ "consumeToken",
619
+ "countAttempt",
620
+ "spendUserTokens",
621
+ "deleteUserTokens"
622
+ ]
611
623
  };
612
624
  function assertStores(stores, where) {
613
625
  if (typeof stores !== "object" || stores === null) {
@@ -833,6 +845,12 @@ function refuseUnusable(token, now, where, noun) {
833
845
  var unknownOneTime = (where, noun) => new TokenError2("TOKEN_UNKNOWN", `${where}: no such ${noun}`, {
834
846
  operation: where
835
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
+ }
836
854
  async function spendOneTime(context, secret, kind, where, noun) {
837
855
  const now = context.clock.now();
838
856
  const token = await context.store.tokens.consumeToken(hashSecret(secret), kind, now);
@@ -875,6 +893,98 @@ async function insertOneTime(context, token, code) {
875
893
  return { secret, expiresAt };
876
894
  }
877
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
+
878
988
  // src/auth/totp.ts
879
989
  import { createHmac, randomBytes as randomBytes4 } from "node:crypto";
880
990
  var TOTP = { digits: 6, stepMs: 30000, drift: 1 };
@@ -990,7 +1100,12 @@ function challengeFlows(context, type, at) {
990
1100
  address: "",
991
1101
  ttlMs: configured.challengeTtlMs
992
1102
  });
993
- return { status: "secondFactor", challenge: secret, expiresAt };
1103
+ return {
1104
+ status: "secondFactor",
1105
+ challenge: secret,
1106
+ expiresAt,
1107
+ userId: record.id
1108
+ };
994
1109
  },
995
1110
  async confirm(challenge, code) {
996
1111
  const where = at("secondFactor.confirm");
@@ -998,8 +1113,9 @@ function challengeFlows(context, type, at) {
998
1113
  const secret = String(challenge);
999
1114
  const { token, attemptsLeft } = await countCodeAttempt(context, secret, "secondFactor", where, type.name);
1000
1115
  const record = await findRecord(context, token.userId, type.name);
1001
- if (record === null)
1002
- throw unknownOneTime(where, "challenge");
1116
+ if (record === null) {
1117
+ throw await unknownChallenge(context, token, secret, where);
1118
+ }
1003
1119
  if (!record.active) {
1004
1120
  await spend(secret, where);
1005
1121
  throw new UserInactiveError2(`${where}: the user is inactive`, {
@@ -1116,6 +1232,7 @@ function signInCodeFlows(context, type, at, finish) {
1116
1232
  address: String(record.fields[type.email]),
1117
1233
  ttlMs: context.config.tokenTtlMs.signInCode
1118
1234
  });
1235
+ await context.store.tokens.spendUserTokens(record.id, "signInCode", context.clock.now(), hashSecret(secret));
1119
1236
  return {
1120
1237
  code,
1121
1238
  challenge: secret,
@@ -1129,8 +1246,9 @@ function signInCodeFlows(context, type, at, finish) {
1129
1246
  const secret = String(challenge);
1130
1247
  const { token, attemptsLeft } = await countCodeAttempt(context, secret, "signInCode", where, type.name);
1131
1248
  const user = await findRecord(context, token.userId, type.name);
1132
- if (user === null)
1133
- throw unknownOneTime(where, "challenge");
1249
+ if (user === null) {
1250
+ throw await unknownChallenge(context, token, secret, where);
1251
+ }
1134
1252
  if (!codeMatches(token, secret, String(code))) {
1135
1253
  if (token.attempts === CODE_ATTEMPTS) {
1136
1254
  await burnOneTime(context, secret, "signInCode");
@@ -1185,37 +1303,6 @@ function typeApi(context, type) {
1185
1303
  updatedAt: now
1186
1304
  });
1187
1305
  };
1188
- const redeem = async (kind, secret, where) => {
1189
- const token = await spendOneTime(context, secret, kind, where, "token");
1190
- const user = await findRecord(context, token.userId, type.name);
1191
- if (user === null)
1192
- throw unknownOneTime(where, "token");
1193
- refuseStale(type, user, token, where, "token");
1194
- return { token, user };
1195
- };
1196
- const issue = async (kind, user, where) => {
1197
- const email = emailOf(type, user.fields);
1198
- if (email === null) {
1199
- throw new NotFoundError2(`${where}: the user has no e-mail`, {
1200
- userId: user.id,
1201
- userType: type.name,
1202
- operation: where
1203
- });
1204
- }
1205
- const { secret, expiresAt } = await issueOneTime(context, {
1206
- kind,
1207
- userId: user.id,
1208
- address: email,
1209
- ttlMs: context.config.tokenTtlMs[kind]
1210
- });
1211
- return { token: secret, email, expiresAt };
1212
- };
1213
- const passwordRule = (where) => {
1214
- if (type.password === null) {
1215
- throw new TypeError(`${where}: the ${type.name} type does not sign in with a password — add password: { login } to it`);
1216
- }
1217
- return type.password;
1218
- };
1219
1306
  return {
1220
1307
  async create(input) {
1221
1308
  return toUser(await insert(input, at("create")));
@@ -1275,7 +1362,7 @@ function typeApi(context, type) {
1275
1362
  },
1276
1363
  async signUp(input) {
1277
1364
  const where = at("signUp");
1278
- passwordRule(where);
1365
+ passwordRule(type, where);
1279
1366
  if (input?.password === undefined) {
1280
1367
  checkPassword(type, undefined, where);
1281
1368
  }
@@ -1284,7 +1371,7 @@ function typeApi(context, type) {
1284
1371
  },
1285
1372
  async signIn(input) {
1286
1373
  const where = at("signIn");
1287
- const rule = passwordRule(where);
1374
+ const rule = passwordRule(type, where);
1288
1375
  const hasher = requireHasher(context, where);
1289
1376
  const login = input?.[rule.login];
1290
1377
  const password = input?.password;
@@ -1303,28 +1390,33 @@ function typeApi(context, type) {
1303
1390
  userType: type.name
1304
1391
  });
1305
1392
  }
1306
- return secondFactor.finish(await rehashed(context, record, String(password)), where);
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;
1307
1399
  },
1308
1400
  async findByLogin(login) {
1309
- const rule = passwordRule(at("findByLogin"));
1401
+ const rule = passwordRule(type, at("findByLogin"));
1310
1402
  const record = await byLogin(rule.normalize(login));
1311
1403
  return record === null ? null : toUser(record);
1312
1404
  },
1313
1405
  async setPassword(user, password, options) {
1314
1406
  const where = at("setPassword");
1315
- passwordRule(where);
1407
+ passwordRule(type, where);
1316
1408
  checkPassword(type, password, where);
1317
1409
  const hash = await requireHasher(context, where).hash(password);
1318
- return toUser(await writeUser(context, user, type, options, where, (_, now) => ({
1319
- password: { hash, updatedAt: now }
1320
- })));
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);
1321
1413
  },
1322
1414
  async changePassword(user, change, options) {
1323
1415
  const where = at("changePassword");
1324
- passwordRule(where);
1416
+ passwordRule(type, where);
1325
1417
  checkPassword(type, change?.next, where);
1326
1418
  const hasher = requireHasher(context, where);
1327
- 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) => {
1328
1420
  if (record.password === null || !await passwordMatches(context, record, change.current, where)) {
1329
1421
  throw new CredentialError2("CREDENTIALS_INVALID", `${where}: the current password does not match`, {
1330
1422
  reason: record.password === null ? "noPassword" : "wrongPassword",
@@ -1338,48 +1430,13 @@ function typeApi(context, type) {
1338
1430
  updatedAt: now
1339
1431
  }
1340
1432
  };
1341
- }));
1433
+ });
1434
+ await endSignInsWaiting(context, written.id);
1435
+ return toUser(written);
1342
1436
  },
1343
1437
  secondFactor: secondFactor.api,
1344
1438
  signInCode,
1345
- verifyEmail: {
1346
- async send(user) {
1347
- const where = at("verifyEmail.send");
1348
- const record = await getRecord(context, idOf(user), type.name, where);
1349
- return issue("verifyEmail", record, where);
1350
- },
1351
- async confirm(secret) {
1352
- const where = at("verifyEmail.confirm");
1353
- const { user } = await redeem("verifyEmail", secret, where);
1354
- return toUser(await writeUser(context, user.id, type, undefined, where, (_, now) => ({
1355
- emailVerifiedAt: now
1356
- })));
1357
- }
1358
- },
1359
- resetPassword: {
1360
- async request(email) {
1361
- const where = at("resetPassword.request");
1362
- passwordRule(where);
1363
- const record = await holderOfEmail(context, type, String(email));
1364
- if (record === null)
1365
- return null;
1366
- const issued = await issue("resetPassword", record, where);
1367
- return { ...issued, user: toUser(record) };
1368
- },
1369
- async confirm(secret, password) {
1370
- const where = at("resetPassword.confirm");
1371
- passwordRule(where);
1372
- checkPassword(type, password, where);
1373
- const hash = await requireHasher(context, where).hash(password);
1374
- const { user } = await redeem("resetPassword", secret, where);
1375
- const written = await writeUser(context, user.id, type, undefined, where, (record, now) => ({
1376
- password: { hash, updatedAt: now },
1377
- ...record.emailVerifiedAt === null ? { emailVerifiedAt: now } : {}
1378
- }));
1379
- await store.sessions.revokeUserSessions(written.id, clock.now());
1380
- return toUser(written);
1381
- }
1382
- }
1439
+ ...emailFlows(context, type, at)
1383
1440
  };
1384
1441
  }
1385
1442
 
@@ -1455,5 +1512,5 @@ export {
1455
1512
  systemClock
1456
1513
  };
1457
1514
 
1458
- //# debugId=9A2F9DDAD478673A64756E2164756E21
1515
+ //# debugId=8A3012761E63D76664756E2164756E21
1459
1516
  //# sourceMappingURL=index.js.map