@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.
- package/README.md +52 -14
- package/dist/auth/context.d.ts +6 -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/one-time.d.ts +7 -0
- 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.map +1 -1
- package/dist/auth/sign-in-code.d.ts +6 -3
- package/dist/auth/sign-in-code.d.ts.map +1 -1
- package/dist/auth/types.d.ts +6 -0
- package/dist/auth/types.d.ts.map +1 -1
- 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 +145 -88
- package/dist/index.js.map +11 -9
- package/docs/README.md +1 -1
- package/docs/guide/adapters.md +56 -3
- package/docs/guide/email-flows.md +7 -1
- package/docs/guide/second-factor.md +27 -2
- package/docs/guide/sign-in-code.md +26 -8
- package/docs/guide/users.md +3 -3
- package/docs/guide/vocabulary.md +1 -0
- package/docs/roadmap.md +12 -4
- package/docs/troubleshooting.md +37 -26
- 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,
|
|
@@ -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: [
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
1515
|
+
//# debugId=8A3012761E63D76664756E2164756E21
|
|
1459
1516
|
//# sourceMappingURL=index.js.map
|