@nxgt/janus 0.22.1 → 0.23.1
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 +97 -16
- package/dist/auth/config/janus-config.d.ts +2 -0
- package/dist/auth/config/janus-config.d.ts.map +1 -1
- package/dist/auth/config/resolved-config.d.ts +1 -0
- package/dist/auth/config/resolved-config.d.ts.map +1 -1
- package/dist/auth/config/warn-throttle-window.d.ts.map +1 -1
- package/dist/auth/email-flows/change-email-token.d.ts +42 -0
- package/dist/auth/email-flows/change-email-token.d.ts.map +1 -0
- package/dist/auth/email-flows/change-email.d.ts +11 -0
- package/dist/auth/email-flows/change-email.d.ts.map +1 -0
- package/dist/auth/email-flows/flows.d.ts +5 -5
- package/dist/auth/email-flows/flows.d.ts.map +1 -1
- package/dist/auth/events.d.ts +3 -2
- package/dist/auth/events.d.ts.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/mail-requests.d.ts +6 -3
- package/dist/auth/mail-requests.d.ts.map +1 -1
- package/dist/auth/one-time/issue.d.ts +7 -1
- package/dist/auth/one-time/issue.d.ts.map +1 -1
- package/dist/auth/port/types/tokens.d.ts +10 -4
- package/dist/auth/port/types/tokens.d.ts.map +1 -1
- package/dist/auth/types/change-email.d.ts +51 -0
- package/dist/auth/types/change-email.d.ts.map +1 -0
- package/dist/auth/types/index.d.ts +1 -0
- package/dist/auth/types/index.d.ts.map +1 -1
- package/dist/auth/types/janus.d.ts +2 -1
- package/dist/auth/types/janus.d.ts.map +1 -1
- package/dist/auth/types/user-type.d.ts +3 -1
- package/dist/auth/types/user-type.d.ts.map +1 -1
- package/dist/auth/users/any-type-api.d.ts +2 -2
- package/dist/auth/users/any-type-api.d.ts.map +1 -1
- package/dist/auth/users/update.d.ts +2 -1
- package/dist/auth/users/update.d.ts.map +1 -1
- package/dist/conformance/cases/tokens/change-email.d.ts +8 -0
- package/dist/conformance/cases/tokens/change-email.d.ts.map +1 -0
- package/dist/conformance/cases/tokens/index.d.ts.map +1 -1
- package/dist/conformance/cases/tokens/kinds.d.ts.map +1 -1
- package/dist/conformance/index.js +92 -57
- package/dist/conformance/index.js.map +6 -5
- package/dist/errors/janus-error/codes.d.ts +7 -3
- package/dist/errors/janus-error/codes.d.ts.map +1 -1
- package/dist/index.js +134 -31
- package/dist/index.js.map +13 -11
- package/docs/README.md +1 -1
- package/docs/guide/adapters.md +13 -7
- package/docs/guide/email-flows.md +98 -12
- package/docs/guide/errors.md +3 -3
- package/docs/guide/events.md +5 -4
- package/docs/guide/users.md +10 -8
- package/docs/guide/vocabulary.md +3 -3
- package/docs/roadmap.md +13 -10
- package/docs/troubleshooting.md +32 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -425,7 +425,8 @@ function resolveTokenTtls(config, where) {
|
|
|
425
425
|
resetPassword: parseDuration(config.tokens?.resetPassword ?? "1h", `${where}: tokens.resetPassword`),
|
|
426
426
|
signInCode: parseDuration(config.tokens?.signInCode ?? "10m", `${where}: tokens.signInCode`),
|
|
427
427
|
magicLink: parseDuration(config.tokens?.magicLink ?? "15m", `${where}: tokens.magicLink`),
|
|
428
|
-
stepUp: parseDuration(config.tokens?.stepUp ?? "10m", `${where}: tokens.stepUp`)
|
|
428
|
+
stepUp: parseDuration(config.tokens?.stepUp ?? "10m", `${where}: tokens.stepUp`),
|
|
429
|
+
changeEmail: parseDuration(config.tokens?.changeEmail ?? "1h", `${where}: tokens.changeEmail`)
|
|
429
430
|
};
|
|
430
431
|
}
|
|
431
432
|
// src/auth/config/warn-throttle-window.ts
|
|
@@ -434,7 +435,8 @@ var FLOWS = [
|
|
|
434
435
|
"signInCode",
|
|
435
436
|
"resetPassword",
|
|
436
437
|
"verifyEmail",
|
|
437
|
-
"stepUp"
|
|
438
|
+
"stepUp",
|
|
439
|
+
"changeEmail"
|
|
438
440
|
];
|
|
439
441
|
var UNITS2 = [
|
|
440
442
|
["d", 86400000],
|
|
@@ -1151,13 +1153,13 @@ function codeMatches(token, challenge, code) {
|
|
|
1151
1153
|
// src/auth/one-time/issue.ts
|
|
1152
1154
|
import { randomInt } from "node:crypto";
|
|
1153
1155
|
async function issueOneTime(context, token) {
|
|
1154
|
-
return insertOneTime(context, token, null);
|
|
1156
|
+
return insertOneTime(context, token, null, token.boundHash ?? null);
|
|
1155
1157
|
}
|
|
1156
1158
|
async function issueCode(context, token) {
|
|
1157
1159
|
const code = String(randomInt(1e6)).padStart(6, "0");
|
|
1158
|
-
return { ...await insertOneTime(context, token, code), code };
|
|
1160
|
+
return { ...await insertOneTime(context, token, code, null), code };
|
|
1159
1161
|
}
|
|
1160
|
-
async function insertOneTime(context, token, code) {
|
|
1162
|
+
async function insertOneTime(context, token, code, boundHash) {
|
|
1161
1163
|
const now = context.clock.now();
|
|
1162
1164
|
const secret = token.secret ?? mintSecret();
|
|
1163
1165
|
const expiresAt = new Date(now.getTime() + token.ttlMs);
|
|
@@ -1166,7 +1168,7 @@ async function insertOneTime(context, token, code) {
|
|
|
1166
1168
|
kind: token.kind,
|
|
1167
1169
|
userId: token.userId,
|
|
1168
1170
|
address: token.address,
|
|
1169
|
-
codeHash: code === null ?
|
|
1171
|
+
codeHash: code === null ? boundHash : hashCode(secret, code),
|
|
1170
1172
|
attempts: 0,
|
|
1171
1173
|
expiresAt,
|
|
1172
1174
|
spentAt: null,
|
|
@@ -1243,8 +1245,11 @@ async function insertLink(links, tokenHash) {
|
|
|
1243
1245
|
}
|
|
1244
1246
|
// src/auth/mail-requests.ts
|
|
1245
1247
|
var PURPOSE2 = "janus/mail/requests/v1";
|
|
1246
|
-
async function countMailByAddress(context, type, flow, email, where) {
|
|
1247
|
-
await countMail(context, type, flow, {
|
|
1248
|
+
async function countMailByAddress(context, type, flow, email, where, userId) {
|
|
1249
|
+
await countMail(context, type, flow, {
|
|
1250
|
+
address: normalizeEmail(email),
|
|
1251
|
+
...userId === undefined ? {} : { userId }
|
|
1252
|
+
}, where);
|
|
1248
1253
|
}
|
|
1249
1254
|
async function countMailByUser(context, type, flow, userId, where) {
|
|
1250
1255
|
await countMail(context, type, flow, { userId }, where);
|
|
@@ -1265,14 +1270,122 @@ async function countMail(context, type, flow, counted, where) {
|
|
|
1265
1270
|
if (attempts <= throttle.attempts)
|
|
1266
1271
|
return;
|
|
1267
1272
|
const now = context.clock.now().getTime();
|
|
1268
|
-
|
|
1269
|
-
throw new MailThrottledError2(`${where}: too many e-mails asked for this ${byUser ? "user" : "address"} — the last one sent still works; use it, or wait for the next window`, {
|
|
1273
|
+
throw new MailThrottledError2(`${where}: too many e-mails asked for this ${"address" in counted ? "address" : "user"} — the last one sent still works; use it, or wait for the next window`, {
|
|
1270
1274
|
userType: type.name,
|
|
1271
|
-
...
|
|
1275
|
+
...counted.userId === undefined ? {} : { userId: counted.userId },
|
|
1272
1276
|
retryAfter: Math.max(1, Math.ceil((endsAt - now) / 1000))
|
|
1273
1277
|
});
|
|
1274
1278
|
}
|
|
1275
1279
|
|
|
1280
|
+
// src/auth/users/fields-patch.ts
|
|
1281
|
+
var normalized = (email) => email === null ? null : normalizeEmail(email);
|
|
1282
|
+
function emailChanged(type, before, after) {
|
|
1283
|
+
return normalized(emailOf(type, before)) !== normalized(emailOf(type, after));
|
|
1284
|
+
}
|
|
1285
|
+
async function fieldsPatch(type, record, patch, where) {
|
|
1286
|
+
const fields = await validateFields(type, { ...record.fields, ...patch }, where);
|
|
1287
|
+
return {
|
|
1288
|
+
fields,
|
|
1289
|
+
logins: loginsOf(type, fields, where),
|
|
1290
|
+
schemaVersion: type.schemaVersion,
|
|
1291
|
+
...emailChanged(type, record.fields, fields) ? { emailVerifiedAt: null } : {}
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/auth/email-flows/change-email-token.ts
|
|
1296
|
+
var formerHash = (secret, former) => hashSecret(`janus/changeEmail/former\x00${secret}\x00${normalizeEmail(former ?? "")}`);
|
|
1297
|
+
async function newAddressOf(type, user, email, where) {
|
|
1298
|
+
const fields = await validateFields(type, { ...user.fields, [type.email]: email }, where);
|
|
1299
|
+
const address = emailOf(type, fields);
|
|
1300
|
+
if (address === null) {
|
|
1301
|
+
throw new TypeError(`janus: email "${type.email}" did not name a string in validated ${type.name} fields — the schema must keep the e-mail field a required string`);
|
|
1302
|
+
}
|
|
1303
|
+
if (normalizeEmail(address) === normalizeEmail(emailOf(type, user.fields) ?? "")) {
|
|
1304
|
+
throw new UserInvalidError2(`${where}: the new e-mail is the user's current one — a change moves to another address; verifyEmail proves the current one`, {
|
|
1305
|
+
issues: [
|
|
1306
|
+
{ path: [type.email], message: "is the current e-mail of the user" }
|
|
1307
|
+
],
|
|
1308
|
+
userType: type.name
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
return address;
|
|
1312
|
+
}
|
|
1313
|
+
async function issueChangeEmailToken(context, type, user, address) {
|
|
1314
|
+
const secret = mintSecret();
|
|
1315
|
+
const { expiresAt } = await issueOneTime(context, {
|
|
1316
|
+
kind: "changeEmail",
|
|
1317
|
+
userId: user.id,
|
|
1318
|
+
address,
|
|
1319
|
+
ttlMs: context.config.tokenTtlMs.changeEmail,
|
|
1320
|
+
secret,
|
|
1321
|
+
boundHash: formerHash(secret, emailOf(type, user.fields))
|
|
1322
|
+
});
|
|
1323
|
+
await context.store.tokens.spendUserTokens(user.id, "changeEmail", context.clock.now(), hashSecret(secret));
|
|
1324
|
+
return { token: secret, email: address, expiresAt };
|
|
1325
|
+
}
|
|
1326
|
+
function refuseMoved(type, user, token, secret, where) {
|
|
1327
|
+
if (token.codeHash !== formerHash(secret, emailOf(type, user.fields))) {
|
|
1328
|
+
throw new TokenError2("TOKEN_STALE", `${where}: the token was asked from an e-mail the user no longer has`, { operation: where, userId: user.id, userType: type.name });
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
async function redeemChangeEmailToken(context, type, secret, where) {
|
|
1332
|
+
const token = await spendOneTime(context, secret, "changeEmail", where, "token");
|
|
1333
|
+
const user = await findRecord(context, token.userId, type.name);
|
|
1334
|
+
if (user === null)
|
|
1335
|
+
throw unknownOneTime(where, "token");
|
|
1336
|
+
refuseMoved(type, user, token, secret, where);
|
|
1337
|
+
return { token, user };
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/auth/email-flows/change-email.ts
|
|
1341
|
+
var inactive = (type, user, where) => new UserInactiveError2(`${where}: the user is inactive`, {
|
|
1342
|
+
userId: user.id,
|
|
1343
|
+
userType: type.name
|
|
1344
|
+
});
|
|
1345
|
+
function changeEmailFlows(context, type, at) {
|
|
1346
|
+
return {
|
|
1347
|
+
async send(user, email) {
|
|
1348
|
+
const where = at("changeEmail.send");
|
|
1349
|
+
const record = await getRecord(context, idOf(user), type.name, where);
|
|
1350
|
+
if (!record.active)
|
|
1351
|
+
throw inactive(type, record, where);
|
|
1352
|
+
const address = await newAddressOf(type, record, email, where);
|
|
1353
|
+
await countMailByUser(context, type, "changeEmail", record.id, where);
|
|
1354
|
+
await countMailByAddress(context, type, "changeEmail", address, where, record.id);
|
|
1355
|
+
return issueChangeEmailToken(context, type, record, address);
|
|
1356
|
+
},
|
|
1357
|
+
async confirm(secret) {
|
|
1358
|
+
return confirmChange(context, type, secret, at("changeEmail.confirm"));
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
async function confirmChange(context, type, secret, where) {
|
|
1363
|
+
const { token, user } = await redeemChangeEmailToken(context, type, secret, where);
|
|
1364
|
+
if (!user.active)
|
|
1365
|
+
throw inactive(type, user, where);
|
|
1366
|
+
const seen = { replaced: null };
|
|
1367
|
+
const written = await writeUser(context, user.id, type, undefined, where, async (record, now) => {
|
|
1368
|
+
if (!record.active)
|
|
1369
|
+
throw inactive(type, record, where);
|
|
1370
|
+
refuseMoved(type, record, token, secret, where);
|
|
1371
|
+
seen.replaced = record;
|
|
1372
|
+
const patch = await fieldsPatch(type, record, { [type.email]: token.address }, where);
|
|
1373
|
+
const proved = record.emailVerifiedAt === null || emailChanged(type, record.fields, patch.fields);
|
|
1374
|
+
return proved ? { ...patch, emailVerifiedAt: now } : patch;
|
|
1375
|
+
});
|
|
1376
|
+
const { replaced } = seen;
|
|
1377
|
+
const moved = replaced !== null && emailChanged(type, replaced.fields, written.fields);
|
|
1378
|
+
if (replaced !== null && moved) {
|
|
1379
|
+
await emit(context, "user.emailChanged", written, written.updatedAt, {
|
|
1380
|
+
formerEmail: emailOf(type, replaced.fields)
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
if (moved || replaced?.emailVerifiedAt === null) {
|
|
1384
|
+
await emit(context, "user.emailVerified", written, written.updatedAt);
|
|
1385
|
+
}
|
|
1386
|
+
return toUser(written);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1276
1389
|
// src/auth/password-written.ts
|
|
1277
1390
|
async function endWhatThePasswordOpened(context, userId) {
|
|
1278
1391
|
const now = context.clock.now();
|
|
@@ -1581,7 +1694,8 @@ function verifyEmailFlows(context, type, at) {
|
|
|
1581
1694
|
function emailFlows(context, type, at) {
|
|
1582
1695
|
return {
|
|
1583
1696
|
verifyEmail: verifyEmailFlows(context, type, at),
|
|
1584
|
-
resetPassword: resetPasswordFlows(context, type, at)
|
|
1697
|
+
resetPassword: resetPasswordFlows(context, type, at),
|
|
1698
|
+
changeEmail: changeEmailFlows(context, type, at)
|
|
1585
1699
|
};
|
|
1586
1700
|
}
|
|
1587
1701
|
// src/auth/email-sign-in.ts
|
|
@@ -2471,21 +2585,6 @@ async function readBatch(context, ids) {
|
|
|
2471
2585
|
return records;
|
|
2472
2586
|
}
|
|
2473
2587
|
|
|
2474
|
-
// src/auth/users/fields-patch.ts
|
|
2475
|
-
var normalized = (email) => email === null ? null : normalizeEmail(email);
|
|
2476
|
-
function emailChanged(type, before, after) {
|
|
2477
|
-
return normalized(emailOf(type, before)) !== normalized(emailOf(type, after));
|
|
2478
|
-
}
|
|
2479
|
-
async function fieldsPatch(type, record, patch, where) {
|
|
2480
|
-
const fields = await validateFields(type, { ...record.fields, ...patch }, where);
|
|
2481
|
-
return {
|
|
2482
|
-
fields,
|
|
2483
|
-
logins: loginsOf(type, fields, where),
|
|
2484
|
-
schemaVersion: type.schemaVersion,
|
|
2485
|
-
...emailChanged(type, record.fields, fields) ? { emailVerifiedAt: null } : {}
|
|
2486
|
-
};
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
2588
|
// src/auth/users/update.ts
|
|
2490
2589
|
async function update(context, type, user, patch, options, where) {
|
|
2491
2590
|
const seen = { replaced: null };
|
|
@@ -2495,9 +2594,13 @@ async function update(context, type, user, patch, options, where) {
|
|
|
2495
2594
|
});
|
|
2496
2595
|
const { replaced } = seen;
|
|
2497
2596
|
if (replaced !== null && emailChanged(type, replaced.fields, written.fields)) {
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
}
|
|
2597
|
+
try {
|
|
2598
|
+
await context.store.tokens.spendUserTokens(written.id, "changeEmail", context.clock.now());
|
|
2599
|
+
} finally {
|
|
2600
|
+
await emit(context, "user.emailChanged", written, written.updatedAt, {
|
|
2601
|
+
formerEmail: emailOf(type, replaced.fields)
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2501
2604
|
}
|
|
2502
2605
|
return written;
|
|
2503
2606
|
}
|
|
@@ -2648,5 +2751,5 @@ export {
|
|
|
2648
2751
|
systemClock
|
|
2649
2752
|
};
|
|
2650
2753
|
|
|
2651
|
-
//# debugId=
|
|
2754
|
+
//# debugId=33064E7A9A36409664756E2164756E21
|
|
2652
2755
|
//# sourceMappingURL=index.js.map
|