@proteinjs/user-server 1.22.0 → 1.22.2
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/CHANGELOG.md +24 -0
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/src/authentication/PasswordHasher.d.ts +9 -0
- package/dist/src/authentication/PasswordHasher.d.ts.map +1 -1
- package/dist/src/authentication/PasswordHasher.js +25 -0
- package/dist/src/authentication/PasswordHasher.js.map +1 -1
- package/dist/src/authentication/PasswordResetToken.d.ts +43 -12
- package/dist/src/authentication/PasswordResetToken.d.ts.map +1 -1
- package/dist/src/authentication/PasswordResetToken.js +86 -21
- package/dist/src/authentication/PasswordResetToken.js.map +1 -1
- package/dist/src/authentication/authenticate.d.ts +15 -0
- package/dist/src/authentication/authenticate.d.ts.map +1 -1
- package/dist/src/authentication/authenticate.js +55 -20
- package/dist/src/authentication/authenticate.js.map +1 -1
- package/dist/src/routes/devLogin.d.ts +1 -1
- package/dist/src/routes/devLogin.js +1 -1
- package/dist/src/routes/executePasswordReset.d.ts +6 -1
- package/dist/src/routes/executePasswordReset.d.ts.map +1 -1
- package/dist/src/routes/executePasswordReset.js +16 -7
- package/dist/src/routes/executePasswordReset.js.map +1 -1
- package/dist/src/routes/initiatePasswordReset.d.ts +10 -2
- package/dist/src/routes/initiatePasswordReset.d.ts.map +1 -1
- package/dist/src/routes/initiatePasswordReset.js +55 -34
- package/dist/src/routes/initiatePasswordReset.js.map +1 -1
- package/dist/src/routes/login.d.ts +8 -0
- package/dist/src/routes/login.d.ts.map +1 -1
- package/dist/src/routes/login.js +63 -22
- package/dist/src/routes/login.js.map +1 -1
- package/dist/src/throttle/ClientAddress.d.ts +35 -0
- package/dist/src/throttle/ClientAddress.d.ts.map +1 -0
- package/dist/src/throttle/ClientAddress.js +48 -0
- package/dist/src/throttle/ClientAddress.js.map +1 -0
- package/dist/src/throttle/PasswordResetThrottle.d.ts +39 -0
- package/dist/src/throttle/PasswordResetThrottle.d.ts.map +1 -0
- package/dist/src/throttle/PasswordResetThrottle.js +59 -0
- package/dist/src/throttle/PasswordResetThrottle.js.map +1 -0
- package/dist/src/throttle/RequestDigests.d.ts +42 -0
- package/dist/src/throttle/RequestDigests.d.ts.map +1 -0
- package/dist/src/throttle/RequestDigests.js +96 -0
- package/dist/src/throttle/RequestDigests.js.map +1 -0
- package/dist/src/throttle/SignInThrottle.d.ts +67 -0
- package/dist/src/throttle/SignInThrottle.d.ts.map +1 -0
- package/dist/src/throttle/SignInThrottle.js +91 -0
- package/dist/src/throttle/SignInThrottle.js.map +1 -0
- package/dist/src/throttle/SlidingWindow.d.ts +51 -0
- package/dist/src/throttle/SlidingWindow.d.ts.map +1 -0
- package/dist/src/throttle/SlidingWindow.js +85 -0
- package/dist/src/throttle/SlidingWindow.js.map +1 -0
- package/dist/test/ClientAddress.test.d.ts +2 -0
- package/dist/test/ClientAddress.test.d.ts.map +1 -0
- package/dist/test/ClientAddress.test.js +51 -0
- package/dist/test/ClientAddress.test.js.map +1 -0
- package/dist/test/DbTraffic.d.ts +29 -0
- package/dist/test/DbTraffic.d.ts.map +1 -0
- package/dist/test/DbTraffic.js +106 -0
- package/dist/test/DbTraffic.js.map +1 -0
- package/dist/test/DevLoginBootstrapAdmin.test.js +3 -4
- package/dist/test/DevLoginBootstrapAdmin.test.js.map +1 -1
- package/dist/test/DevLoginStaleCookieFirstHit.test.js +1 -1
- package/dist/test/DevLoginStaleCookieFirstHit.test.js.map +1 -1
- package/dist/test/ExecutePasswordReset.test.js +513 -124
- package/dist/test/ExecutePasswordReset.test.js.map +1 -1
- package/dist/test/InitiatePasswordReset.test.d.ts +2 -0
- package/dist/test/InitiatePasswordReset.test.d.ts.map +1 -0
- package/dist/test/InitiatePasswordReset.test.js +783 -0
- package/dist/test/InitiatePasswordReset.test.js.map +1 -0
- package/dist/test/LogCapture.d.ts +22 -0
- package/dist/test/LogCapture.d.ts.map +1 -0
- package/dist/test/LogCapture.js +127 -0
- package/dist/test/LogCapture.js.map +1 -0
- package/dist/test/LoginThrottle.integration.test.d.ts +2 -0
- package/dist/test/LoginThrottle.integration.test.d.ts.map +1 -0
- package/dist/test/LoginThrottle.integration.test.js +616 -0
- package/dist/test/LoginThrottle.integration.test.js.map +1 -0
- package/dist/test/MalformedResetTokens.d.ts +11 -0
- package/dist/test/MalformedResetTokens.d.ts.map +1 -0
- package/dist/test/MalformedResetTokens.js +35 -0
- package/dist/test/MalformedResetTokens.js.map +1 -0
- package/dist/test/RequestDigests.test.d.ts +2 -0
- package/dist/test/RequestDigests.test.d.ts.map +1 -0
- package/dist/test/RequestDigests.test.js +73 -0
- package/dist/test/RequestDigests.test.js.map +1 -0
- package/dist/test/SlidingWindow.test.d.ts +2 -0
- package/dist/test/SlidingWindow.test.d.ts.map +1 -0
- package/dist/test/SlidingWindow.test.js +72 -0
- package/dist/test/SlidingWindow.test.js.map +1 -0
- package/dist/test/ValidateResetToken.test.js +156 -33
- package/dist/test/ValidateResetToken.test.js.map +1 -1
- package/dist/test/emulatorLock.test.d.ts +2 -0
- package/dist/test/emulatorLock.test.d.ts.map +1 -0
- package/dist/test/emulatorLock.test.js +32 -0
- package/dist/test/emulatorLock.test.js.map +1 -0
- package/dist/test/emulatorLockName.test.d.ts +2 -0
- package/dist/test/emulatorLockName.test.d.ts.map +1 -0
- package/dist/test/emulatorLockName.test.js +36 -0
- package/dist/test/emulatorLockName.test.js.map +1 -0
- package/generated/index.ts +1 -1
- package/index.ts +6 -1
- package/jest.config.js +2 -2
- package/package.json +3 -3
- package/src/authentication/PasswordHasher.ts +18 -1
- package/src/authentication/PasswordResetToken.ts +74 -20
- package/src/authentication/authenticate.ts +36 -9
- package/src/routes/devLogin.ts +1 -1
- package/src/routes/executePasswordReset.ts +10 -2
- package/src/routes/initiatePasswordReset.ts +46 -25
- package/src/routes/login.ts +39 -8
- package/src/throttle/ClientAddress.ts +47 -0
- package/src/throttle/PasswordResetThrottle.ts +62 -0
- package/src/throttle/RequestDigests.ts +97 -0
- package/src/throttle/SignInThrottle.ts +98 -0
- package/src/throttle/SlidingWindow.ts +90 -0
- package/test/ClientAddress.test.ts +54 -0
- package/test/DbTraffic.ts +43 -0
- package/test/DevLoginBootstrapAdmin.test.ts +3 -4
- package/test/DevLoginStaleCookieFirstHit.test.ts +1 -1
- package/test/ExecutePasswordReset.test.ts +308 -70
- package/test/InitiatePasswordReset.test.ts +456 -0
- package/test/LogCapture.ts +54 -0
- package/test/LoginThrottle.integration.test.ts +339 -0
- package/test/MalformedResetTokens.ts +27 -0
- package/test/RequestDigests.test.ts +81 -0
- package/test/SlidingWindow.test.ts +85 -0
- package/test/ValidateResetToken.test.ts +108 -24
- package/test/emulatorLock.js +5 -6
- package/test/emulatorLock.test.ts +32 -0
- package/test/emulatorLockName.test.ts +33 -0
- package/test/setup.js +2 -0
|
@@ -2,8 +2,16 @@ import { Route } from '@proteinjs/server-api';
|
|
|
2
2
|
/**
|
|
3
3
|
* Route for initiating a password reset process.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Mints a reset token through `PasswordResetToken` — which stores only the token's digest and
|
|
6
|
+
* its expiry on the user row — and mails the token to the account as a reset link. The token is
|
|
7
|
+
* withdrawn again when the mail fails to send.
|
|
8
|
+
*
|
|
9
|
+
* ONE ANSWER: every request with an address is answered `ONE_ANSWER` — BEFORE the address is
|
|
10
|
+
* looked up — whether it has an account, is throttled (`PasswordResetThrottle`: per client and
|
|
11
|
+
* per address), is inside the account's five-minute gap, or its mail fails; so neither the
|
|
12
|
+
* words, the status nor the timing say whether an address has an account. The work runs after
|
|
13
|
+
* the answer. Every request is one outcome line carrying the account digest and the coarse IP
|
|
14
|
+
* hash (`RequestDigests`) — never the address.
|
|
7
15
|
*
|
|
8
16
|
* Requires an implementation of `DefaultPasswordResetEmailConfigFactory` to build the password reset email with.
|
|
9
17
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initiatePasswordReset.d.ts","sourceRoot":"","sources":["../../../src/routes/initiatePasswordReset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"initiatePasswordReset.d.ts","sourceRoot":"","sources":["../../../src/routes/initiatePasswordReset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAoB9C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAoEnC,CAAC"}
|
|
@@ -57,11 +57,27 @@ var logger_1 = require("@proteinjs/logger");
|
|
|
57
57
|
var moment_1 = __importDefault(require("moment"));
|
|
58
58
|
var email_server_1 = require("@proteinjs/email-server");
|
|
59
59
|
var PasswordResetToken_1 = require("../authentication/PasswordResetToken");
|
|
60
|
+
var ClientAddress_1 = require("../throttle/ClientAddress");
|
|
61
|
+
var RequestDigests_1 = require("../throttle/RequestDigests");
|
|
62
|
+
var PasswordResetThrottle_1 = require("../throttle/PasswordResetThrottle");
|
|
63
|
+
/**
|
|
64
|
+
* The door's one answer: the same words for an address with an account or without, inside or
|
|
65
|
+
* past a window, mailed or not — the page shows exactly this sentence.
|
|
66
|
+
*/
|
|
67
|
+
var ONE_ANSWER = { message: 'If that address has an account, a reset link is on its way.' };
|
|
60
68
|
/**
|
|
61
69
|
* Route for initiating a password reset process.
|
|
62
70
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
71
|
+
* Mints a reset token through `PasswordResetToken` — which stores only the token's digest and
|
|
72
|
+
* its expiry on the user row — and mails the token to the account as a reset link. The token is
|
|
73
|
+
* withdrawn again when the mail fails to send.
|
|
74
|
+
*
|
|
75
|
+
* ONE ANSWER: every request with an address is answered `ONE_ANSWER` — BEFORE the address is
|
|
76
|
+
* looked up — whether it has an account, is throttled (`PasswordResetThrottle`: per client and
|
|
77
|
+
* per address), is inside the account's five-minute gap, or its mail fails; so neither the
|
|
78
|
+
* words, the status nor the timing say whether an address has an account. The work runs after
|
|
79
|
+
* the answer. Every request is one outcome line carrying the account digest and the coarse IP
|
|
80
|
+
* hash (`RequestDigests`) — never the address.
|
|
65
81
|
*
|
|
66
82
|
* Requires an implementation of `DefaultPasswordResetEmailConfigFactory` to build the password reset email with.
|
|
67
83
|
*
|
|
@@ -74,65 +90,70 @@ exports.initiatePasswordReset = {
|
|
|
74
90
|
path: user_1.routes.initiatePasswordReset.path,
|
|
75
91
|
method: user_1.routes.initiatePasswordReset.method,
|
|
76
92
|
onRequest: function (request, response) { return __awaiter(void 0, void 0, void 0, function () {
|
|
77
|
-
var logger,
|
|
93
|
+
var logger, requestedEmail, digests, fields, window, email, db, user, resetToken, mintedAt, emailSender, defaultConfigFactory, passwordResetToken, config, _a, text, html, error_1;
|
|
78
94
|
var _b, _c;
|
|
79
95
|
return __generator(this, function (_d) {
|
|
80
96
|
switch (_d.label) {
|
|
81
97
|
case 0:
|
|
82
98
|
logger = new logger_1.Logger({ name: 'initiatePasswordReset' });
|
|
83
|
-
|
|
99
|
+
requestedEmail = ((_b = request.body) !== null && _b !== void 0 ? _b : {}).email;
|
|
100
|
+
if (typeof requestedEmail !== 'string' || requestedEmail.length === 0) {
|
|
101
|
+
response.status(400).send({ error: 'Email cannot be blank' });
|
|
102
|
+
return [2 /*return*/];
|
|
103
|
+
}
|
|
104
|
+
digests = new RequestDigests_1.RequestDigests();
|
|
105
|
+
fields = { account: digests.account(requestedEmail), ip: digests.coarseIp(new ClientAddress_1.ClientAddress().of(request)) };
|
|
106
|
+
window = PasswordResetThrottle_1.passwordResetThrottle.admit(fields.ip, fields.account);
|
|
107
|
+
response.send(ONE_ANSWER);
|
|
108
|
+
if (window) {
|
|
109
|
+
logger.warn({ message: "Password reset throttled", obj: __assign(__assign({}, fields), { window: window }) });
|
|
110
|
+
return [2 /*return*/];
|
|
111
|
+
}
|
|
112
|
+
email = requestedEmail.toLowerCase();
|
|
84
113
|
db = (0, db_1.getDbAsSystem)();
|
|
85
|
-
genericResponse = { message: 'If an account with that email exists, we have sent a password reset link.' };
|
|
86
114
|
return [4 /*yield*/, db.get(user_1.tables.User, { email: email })];
|
|
87
115
|
case 1:
|
|
88
116
|
user = _d.sent();
|
|
89
117
|
if (!user) {
|
|
90
|
-
logger.info({ message: "Password reset requested for non-existent user", obj:
|
|
91
|
-
// Don't reveal that the user doesn't exist
|
|
92
|
-
response.send(genericResponse);
|
|
118
|
+
logger.info({ message: "Password reset requested for non-existent user", obj: fields });
|
|
93
119
|
return [2 /*return*/];
|
|
94
120
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (timeDifference < 5) {
|
|
101
|
-
logger.info({ message: "Password reset requested too soon for user", obj: { email: email } });
|
|
102
|
-
response.send(genericResponse);
|
|
103
|
-
return [2 /*return*/];
|
|
104
|
-
}
|
|
121
|
+
resetToken = new PasswordResetToken_1.PasswordResetToken();
|
|
122
|
+
mintedAt = resetToken.mintedAt(user);
|
|
123
|
+
if (mintedAt && (0, moment_1.default)().diff(mintedAt, 'minutes') < 5) {
|
|
124
|
+
logger.info({ message: "Password reset requested too soon for user", obj: fields });
|
|
125
|
+
return [2 /*return*/];
|
|
105
126
|
}
|
|
106
127
|
emailSender = new email_server_1.EmailSender();
|
|
107
128
|
defaultConfigFactory = (0, email_server_1.getDefaultPasswordResetEmailConfigFactory)();
|
|
108
129
|
if (!defaultConfigFactory) {
|
|
109
130
|
throw new Error("Unable to find a @proteinjs/email-server/DefaultPasswordResetEmailConfigFactory implementation when initiating password reset.");
|
|
110
131
|
}
|
|
111
|
-
|
|
112
|
-
passwordResetTokenExpiration = (0, moment_1.default)().add(1, 'hour');
|
|
113
|
-
_d.label = 2;
|
|
132
|
+
return [4 /*yield*/, resetToken.mint(user)];
|
|
114
133
|
case 2:
|
|
115
|
-
_d.
|
|
134
|
+
passwordResetToken = _d.sent();
|
|
135
|
+
_d.label = 3;
|
|
136
|
+
case 3:
|
|
137
|
+
_d.trys.push([3, 5, , 7]);
|
|
116
138
|
config = defaultConfigFactory.getConfig();
|
|
117
139
|
_a = config.getEmailContent("".concat(user_1.uiRoutes.auth.passwordReset, "?token=").concat(passwordResetToken)), text = _a.text, html = _a.html;
|
|
118
140
|
// Send email containing a reset link
|
|
119
141
|
return [4 /*yield*/, emailSender.sendEmail(__assign({ to: user.email, subject: ((_c = config.options) === null || _c === void 0 ? void 0 : _c.subject) || 'Reset Password', text: text, html: html }, config.options))];
|
|
120
|
-
case 3:
|
|
121
|
-
// Send email containing a reset link
|
|
122
|
-
_d.sent();
|
|
123
|
-
// If email is sent successfully, save reset token to user record
|
|
124
|
-
return [4 /*yield*/, db.update(user_1.tables.User, { id: user.id, passwordResetToken: passwordResetToken, passwordResetTokenExpiration: passwordResetTokenExpiration })];
|
|
125
142
|
case 4:
|
|
126
|
-
//
|
|
143
|
+
// Send email containing a reset link
|
|
127
144
|
_d.sent();
|
|
128
|
-
|
|
129
|
-
return [3 /*break*/,
|
|
145
|
+
logger.info({ message: "Password reset link mailed", obj: fields });
|
|
146
|
+
return [3 /*break*/, 7];
|
|
130
147
|
case 5:
|
|
131
148
|
error_1 = _d.sent();
|
|
132
|
-
logger.error({ message: "Failed to send password reset email", obj:
|
|
133
|
-
|
|
134
|
-
return [
|
|
135
|
-
case 6:
|
|
149
|
+
logger.error({ message: "Failed to send password reset email", obj: fields, error: error_1 });
|
|
150
|
+
// The link never reached the account: withdraw the token so asking again is not throttled
|
|
151
|
+
return [4 /*yield*/, resetToken.revoke(user, passwordResetToken)];
|
|
152
|
+
case 6:
|
|
153
|
+
// The link never reached the account: withdraw the token so asking again is not throttled
|
|
154
|
+
_d.sent();
|
|
155
|
+
return [3 /*break*/, 7];
|
|
156
|
+
case 7: return [2 /*return*/];
|
|
136
157
|
}
|
|
137
158
|
});
|
|
138
159
|
}); },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initiatePasswordReset.js","sourceRoot":"","sources":["../../../src/routes/initiatePasswordReset.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,oCAA8C;AAC9C,wCAA2D;AAC3D,4CAA2C;AAC3C,kDAA4B;AAC5B,wDAGiC;AACjC,2EAA0E;AAE1E
|
|
1
|
+
{"version":3,"file":"initiatePasswordReset.js","sourceRoot":"","sources":["../../../src/routes/initiatePasswordReset.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import { Route } from '@proteinjs/server-api';
|
|
2
|
+
/**
|
|
3
|
+
* `POST /user/login`. Throttled per client and per account (`SignInThrottle`): a throttled try
|
|
4
|
+
* is told "Too many attempts. Try again in a few minutes." whether or not the address has an
|
|
5
|
+
* account, after the same password check a refusal runs (verdict discarded), so neither the
|
|
6
|
+
* words nor the timing tell. Every refusal is a "Sign-in refused" line and every throttled try
|
|
7
|
+
* its own "Sign-in throttled" line, carrying the account digest and the coarse IP hash
|
|
8
|
+
* (`RequestDigests`) — never the address.
|
|
9
|
+
*/
|
|
2
10
|
export declare const login: Route;
|
|
3
11
|
//# sourceMappingURL=login.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../../src/routes/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../../src/routes/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAU9C;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,EAAE,KA+DnB,CAAC"}
|
package/dist/src/routes/login.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
2
13
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
14
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
15
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -38,41 +49,71 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
38
49
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
50
|
exports.login = void 0;
|
|
40
51
|
var user_1 = require("@proteinjs/user");
|
|
52
|
+
var logger_1 = require("@proteinjs/logger");
|
|
41
53
|
var authenticate_1 = require("../authentication/authenticate");
|
|
42
54
|
var establishSession_1 = require("../authentication/establishSession");
|
|
43
55
|
var AccountDeletion_1 = require("../services/AccountDeletion");
|
|
56
|
+
var ClientAddress_1 = require("../throttle/ClientAddress");
|
|
57
|
+
var RequestDigests_1 = require("../throttle/RequestDigests");
|
|
58
|
+
var SignInThrottle_1 = require("../throttle/SignInThrottle");
|
|
59
|
+
/**
|
|
60
|
+
* `POST /user/login`. Throttled per client and per account (`SignInThrottle`): a throttled try
|
|
61
|
+
* is told "Too many attempts. Try again in a few minutes." whether or not the address has an
|
|
62
|
+
* account, after the same password check a refusal runs (verdict discarded), so neither the
|
|
63
|
+
* words nor the timing tell. Every refusal is a "Sign-in refused" line and every throttled try
|
|
64
|
+
* its own "Sign-in throttled" line, carrying the account digest and the coarse IP hash
|
|
65
|
+
* (`RequestDigests`) — never the address.
|
|
66
|
+
*/
|
|
44
67
|
exports.login = {
|
|
45
68
|
path: user_1.routes.login.path,
|
|
46
69
|
method: user_1.routes.login.method,
|
|
47
70
|
onRequest: function (request, response) { return __awaiter(void 0, void 0, void 0, function () {
|
|
48
|
-
var credentials, error, result, outcome, error_1, error;
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
var logger, credentials, email, password, digests, ip, account, fields, window, error, result, outcome, error_1, error;
|
|
72
|
+
var _a;
|
|
73
|
+
return __generator(this, function (_b) {
|
|
74
|
+
switch (_b.label) {
|
|
51
75
|
case 0:
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
logger = new logger_1.Logger({ name: 'login' });
|
|
77
|
+
credentials = (_a = request.body) !== null && _a !== void 0 ? _a : {};
|
|
78
|
+
email = typeof credentials.email === 'string' ? credentials.email : '';
|
|
79
|
+
password = typeof credentials.password === 'string' ? credentials.password : '';
|
|
80
|
+
digests = new RequestDigests_1.RequestDigests();
|
|
81
|
+
ip = digests.coarseIp(new ClientAddress_1.ClientAddress().of(request));
|
|
82
|
+
account = email ? digests.account(email) : undefined;
|
|
83
|
+
fields = account ? { account: account, ip: ip } : { ip: ip };
|
|
84
|
+
window = SignInThrottle_1.signInThrottle.admit(ip, password ? account : undefined);
|
|
85
|
+
if (!window) return [3 /*break*/, 2];
|
|
86
|
+
return [4 /*yield*/, (0, authenticate_1.checkPassword)(email, password)];
|
|
87
|
+
case 1:
|
|
88
|
+
_b.sent();
|
|
89
|
+
logger.warn({ message: 'Sign-in throttled', obj: __assign(__assign({}, fields), { window: window }) });
|
|
90
|
+
response.send({ error: SignInThrottle_1.SignInThrottle.ANSWER });
|
|
91
|
+
return [2 /*return*/];
|
|
92
|
+
case 2:
|
|
93
|
+
if (!email || !password || !account) {
|
|
54
94
|
error = "Email and password cannot be blank";
|
|
55
|
-
|
|
95
|
+
logger.info({ message: 'Sign-in refused', obj: __assign(__assign({}, fields), { reason: error }) });
|
|
56
96
|
response.send({ error: error });
|
|
57
97
|
return [2 /*return*/];
|
|
58
98
|
}
|
|
59
|
-
return [4 /*yield*/, (0, authenticate_1.authenticate)(
|
|
60
|
-
case
|
|
61
|
-
result =
|
|
99
|
+
return [4 /*yield*/, (0, authenticate_1.authenticate)(email, password)];
|
|
100
|
+
case 3:
|
|
101
|
+
result = _b.sent();
|
|
62
102
|
if (result !== true) {
|
|
63
|
-
|
|
103
|
+
logger.info({ message: 'Sign-in refused', obj: __assign(__assign({}, fields), { reason: result }) });
|
|
64
104
|
response.send({ error: result });
|
|
65
105
|
return [2 /*return*/];
|
|
66
106
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
_a.trys.push([2, 4, , 5]);
|
|
70
|
-
return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().cancelPendingDeletion(credentials.email)];
|
|
71
|
-
case 3:
|
|
72
|
-
outcome = _a.sent();
|
|
73
|
-
return [3 /*break*/, 5];
|
|
107
|
+
SignInThrottle_1.signInThrottle.recordSuccess(account, ip);
|
|
108
|
+
_b.label = 4;
|
|
74
109
|
case 4:
|
|
75
|
-
|
|
110
|
+
_b.trys.push([4, 6, , 7]);
|
|
111
|
+
return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().cancelPendingDeletion(email)];
|
|
112
|
+
case 5:
|
|
113
|
+
outcome = _b.sent();
|
|
114
|
+
return [3 /*break*/, 7];
|
|
115
|
+
case 6:
|
|
116
|
+
error_1 = _b.sent();
|
|
76
117
|
// Security boundary: the login response never carries internal error detail — an
|
|
77
118
|
// attacker probing emails must learn nothing from failure shapes (founder ruling
|
|
78
119
|
// 2026-08-18 after a watcher error surfaced verbatim in the login form). The real
|
|
@@ -80,16 +121,16 @@ exports.login = {
|
|
|
80
121
|
console.error('cancelPendingDeletion failed during login', error_1);
|
|
81
122
|
response.send({ error: 'Unable to log in right now. Please try again.' });
|
|
82
123
|
return [2 /*return*/];
|
|
83
|
-
case
|
|
124
|
+
case 7:
|
|
84
125
|
if (outcome === 'purging') {
|
|
85
126
|
error = 'This account is being deleted and can no longer be restored.';
|
|
86
127
|
console.error(error);
|
|
87
128
|
response.send({ error: error });
|
|
88
129
|
return [2 /*return*/];
|
|
89
130
|
}
|
|
90
|
-
return [4 /*yield*/, (0, establishSession_1.establishSession)(request,
|
|
91
|
-
case
|
|
92
|
-
|
|
131
|
+
return [4 /*yield*/, (0, establishSession_1.establishSession)(request, email)];
|
|
132
|
+
case 8:
|
|
133
|
+
_b.sent();
|
|
93
134
|
response.send({});
|
|
94
135
|
return [2 /*return*/];
|
|
95
136
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../../src/routes/login.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../../src/routes/login.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The request fields the read needs — an express request has all of them. */
|
|
2
|
+
type ClientRequest = {
|
|
3
|
+
app?: {
|
|
4
|
+
get?: (setting: string) => unknown;
|
|
5
|
+
};
|
|
6
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
7
|
+
socket?: {
|
|
8
|
+
remoteAddress?: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The client's address, as a throttle should key on it.
|
|
13
|
+
*
|
|
14
|
+
* Behind Google's external Application Load Balancer the `X-Forwarded-For` header ENDS with the
|
|
15
|
+
* two entries the balancer appends — `<client-ip>,<load-balancer-ip>` — and everything before
|
|
16
|
+
* them is whatever the client itself sent. So the client is the second entry from the right:
|
|
17
|
+
* - never the LAST entry: that is the balancer's own forwarding-rule address, the same for every
|
|
18
|
+
* visitor. It is what express's `request.ip` resolves to under the server's `trust proxy 1`
|
|
19
|
+
* (one trusted hop: the Google front end that opened the connection), so a throttle keyed on
|
|
20
|
+
* `request.ip` there is one bucket for the whole world;
|
|
21
|
+
* - never the FIRST entry: a client can send its own header, and the balancer keeps it.
|
|
22
|
+
*
|
|
23
|
+
* Only when the server trusts a proxy (the deployed shape; `trust proxy` is unset in
|
|
24
|
+
* development) is the header read at all. A request that did not come through the balancer —
|
|
25
|
+
* no appended pair: an in-cluster call, a probe — and every request in development key on the
|
|
26
|
+
* connection's own address.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ClientAddress {
|
|
29
|
+
/** How many entries the load balancer appends to the header: the client, then itself. */
|
|
30
|
+
private static readonly BALANCER_APPENDED_ENTRIES;
|
|
31
|
+
of(request: ClientRequest): string;
|
|
32
|
+
private forwardedFor;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=ClientAddress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClientAddress.d.ts","sourceRoot":"","sources":["../../../src/throttle/ClientAddress.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,KAAK,aAAa,GAAG;IACnB,GAAG,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;KAAE,CAAC;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACxD,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrC,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,aAAa;IACxB,yFAAyF;IACzF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAAK;IAEtD,EAAE,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM;IAUlC,OAAO,CAAC,YAAY;CAQrB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClientAddress = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The client's address, as a throttle should key on it.
|
|
6
|
+
*
|
|
7
|
+
* Behind Google's external Application Load Balancer the `X-Forwarded-For` header ENDS with the
|
|
8
|
+
* two entries the balancer appends — `<client-ip>,<load-balancer-ip>` — and everything before
|
|
9
|
+
* them is whatever the client itself sent. So the client is the second entry from the right:
|
|
10
|
+
* - never the LAST entry: that is the balancer's own forwarding-rule address, the same for every
|
|
11
|
+
* visitor. It is what express's `request.ip` resolves to under the server's `trust proxy 1`
|
|
12
|
+
* (one trusted hop: the Google front end that opened the connection), so a throttle keyed on
|
|
13
|
+
* `request.ip` there is one bucket for the whole world;
|
|
14
|
+
* - never the FIRST entry: a client can send its own header, and the balancer keeps it.
|
|
15
|
+
*
|
|
16
|
+
* Only when the server trusts a proxy (the deployed shape; `trust proxy` is unset in
|
|
17
|
+
* development) is the header read at all. A request that did not come through the balancer —
|
|
18
|
+
* no appended pair: an in-cluster call, a probe — and every request in development key on the
|
|
19
|
+
* connection's own address.
|
|
20
|
+
*/
|
|
21
|
+
var ClientAddress = /** @class */ (function () {
|
|
22
|
+
function ClientAddress() {
|
|
23
|
+
}
|
|
24
|
+
ClientAddress.prototype.of = function (request) {
|
|
25
|
+
var _a, _b, _c, _d;
|
|
26
|
+
if ((_b = (_a = request.app) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.call(_a, 'trust proxy')) {
|
|
27
|
+
var entries = this.forwardedFor(request);
|
|
28
|
+
if (entries.length >= ClientAddress.BALANCER_APPENDED_ENTRIES) {
|
|
29
|
+
return entries[entries.length - ClientAddress.BALANCER_APPENDED_ENTRIES];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return (_d = (_c = request.socket) === null || _c === void 0 ? void 0 : _c.remoteAddress) !== null && _d !== void 0 ? _d : '';
|
|
33
|
+
};
|
|
34
|
+
ClientAddress.prototype.forwardedFor = function (request) {
|
|
35
|
+
var _a;
|
|
36
|
+
var header = (_a = request.headers) === null || _a === void 0 ? void 0 : _a['x-forwarded-for'];
|
|
37
|
+
var value = Array.isArray(header) ? header.join(',') : header !== null && header !== void 0 ? header : '';
|
|
38
|
+
return value
|
|
39
|
+
.split(',')
|
|
40
|
+
.map(function (entry) { return entry.trim(); })
|
|
41
|
+
.filter(function (entry) { return entry.length > 0; });
|
|
42
|
+
};
|
|
43
|
+
/** How many entries the load balancer appends to the header: the client, then itself. */
|
|
44
|
+
ClientAddress.BALANCER_APPENDED_ENTRIES = 2;
|
|
45
|
+
return ClientAddress;
|
|
46
|
+
}());
|
|
47
|
+
exports.ClientAddress = ClientAddress;
|
|
48
|
+
//# sourceMappingURL=ClientAddress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClientAddress.js","sourceRoot":"","sources":["../../../src/throttle/ClientAddress.ts"],"names":[],"mappings":";;;AAOA;;;;;;;;;;;;;;;;GAgBG;AACH;IAAA;IAsBA,CAAC;IAlBC,0BAAE,GAAF,UAAG,OAAsB;;QACvB,IAAI,MAAA,MAAA,OAAO,CAAC,GAAG,0CAAE,GAAG,mDAAG,aAAa,CAAC,EAAE;YACrC,IAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;YAC3C,IAAI,OAAO,CAAC,MAAM,IAAI,aAAa,CAAC,yBAAyB,EAAE;gBAC7D,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,aAAa,CAAC,yBAAyB,CAAC,CAAC;aAC1E;SACF;QACD,OAAO,MAAA,MAAA,OAAO,CAAC,MAAM,0CAAE,aAAa,mCAAI,EAAE,CAAC;IAC7C,CAAC;IAEO,oCAAY,GAApB,UAAqB,OAAsB;;QACzC,IAAM,MAAM,GAAG,MAAA,OAAO,CAAC,OAAO,0CAAG,iBAAiB,CAAC,CAAC;QACpD,IAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,aAAN,MAAM,cAAN,MAAM,GAAI,EAAE,CAAC;QACtE,OAAO,KAAK;aACT,KAAK,CAAC,GAAG,CAAC;aACV,GAAG,CAAC,UAAC,KAAK,IAAK,OAAA,KAAK,CAAC,IAAI,EAAE,EAAZ,CAAY,CAAC;aAC5B,MAAM,CAAC,UAAC,KAAK,IAAK,OAAA,KAAK,CAAC,MAAM,GAAG,CAAC,EAAhB,CAAgB,CAAC,CAAC;IACzC,CAAC;IApBD,yFAAyF;IACjE,uCAAyB,GAAG,CAAC,CAAC;IAoBxD,oBAAC;CAAA,AAtBD,IAsBC;AAtBY,sCAAa"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ThrottleWindow } from './SignInThrottle';
|
|
2
|
+
/**
|
|
3
|
+
* The password-reset door's two windows (`POST /user/initiate-password-reset`), keyed by digests
|
|
4
|
+
* (`RequestDigests`), never by an address. Both count every request, whether or not the address
|
|
5
|
+
* has an account — a window that only counted real accounts would itself say which addresses
|
|
6
|
+
* have one. Past either window nothing is looked up, minted or mailed; the door's answer is the
|
|
7
|
+
* same sentence as always (see the route).
|
|
8
|
+
*
|
|
9
|
+
* These sit on top of the account row's own five-minute gap between links, which spans replicas
|
|
10
|
+
* (it is stored); the windows are in process memory (`SlidingWindow`): per replica. A
|
|
11
|
+
* deployment of three replicas behind a load balancer (up to ten under load) keeps three sets
|
|
12
|
+
* of windows, so its effective ceiling is ~3× the numbers below (up to ~10×) — for one address
|
|
13
|
+
* still at most one mail per five minutes. Friction, not the wall — a shared store is a
|
|
14
|
+
* separate step.
|
|
15
|
+
*/
|
|
16
|
+
export declare class PasswordResetThrottle {
|
|
17
|
+
/**
|
|
18
|
+
* Per client: 10 requests an hour. Someone asking for their own address again, or a household
|
|
19
|
+
* asking for a few addresses, passes; a script sweeping addresses stops at ten.
|
|
20
|
+
*/
|
|
21
|
+
private static readonly CLIENT_LIMIT;
|
|
22
|
+
private static readonly CLIENT_WINDOW_MS;
|
|
23
|
+
/**
|
|
24
|
+
* Per address: 3 requests an hour. A person whose mail is slow asks once or twice more; beyond
|
|
25
|
+
* that it is mail aimed at somebody's inbox.
|
|
26
|
+
*/
|
|
27
|
+
private static readonly ACCOUNT_LIMIT;
|
|
28
|
+
private static readonly ACCOUNT_WINDOW_MS;
|
|
29
|
+
private readonly clients;
|
|
30
|
+
private readonly accounts;
|
|
31
|
+
constructor(options?: {
|
|
32
|
+
now?: () => number;
|
|
33
|
+
});
|
|
34
|
+
/** Count this request and answer which window refuses it, if any. */
|
|
35
|
+
admit(client: string, account: string): ThrottleWindow | undefined;
|
|
36
|
+
}
|
|
37
|
+
/** The process-wide windows the reset door shares — they must span requests. */
|
|
38
|
+
export declare const passwordResetThrottle: PasswordResetThrottle;
|
|
39
|
+
//# sourceMappingURL=PasswordResetThrottle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PasswordResetThrottle.d.ts","sourceRoot":"","sources":["../../../src/throttle/PasswordResetThrottle.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAqB;IAChC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAM;IAC1C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAkB;IAE1D;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAK;IAC1C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAkB;IAE3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;gBAE7B,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;KAAE;IAa5C,qEAAqE;IACrE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;CASnE;AAED,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,uBAA8B,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.passwordResetThrottle = exports.PasswordResetThrottle = void 0;
|
|
4
|
+
var SlidingWindow_1 = require("./SlidingWindow");
|
|
5
|
+
/**
|
|
6
|
+
* The password-reset door's two windows (`POST /user/initiate-password-reset`), keyed by digests
|
|
7
|
+
* (`RequestDigests`), never by an address. Both count every request, whether or not the address
|
|
8
|
+
* has an account — a window that only counted real accounts would itself say which addresses
|
|
9
|
+
* have one. Past either window nothing is looked up, minted or mailed; the door's answer is the
|
|
10
|
+
* same sentence as always (see the route).
|
|
11
|
+
*
|
|
12
|
+
* These sit on top of the account row's own five-minute gap between links, which spans replicas
|
|
13
|
+
* (it is stored); the windows are in process memory (`SlidingWindow`): per replica. A
|
|
14
|
+
* deployment of three replicas behind a load balancer (up to ten under load) keeps three sets
|
|
15
|
+
* of windows, so its effective ceiling is ~3× the numbers below (up to ~10×) — for one address
|
|
16
|
+
* still at most one mail per five minutes. Friction, not the wall — a shared store is a
|
|
17
|
+
* separate step.
|
|
18
|
+
*/
|
|
19
|
+
var PasswordResetThrottle = /** @class */ (function () {
|
|
20
|
+
function PasswordResetThrottle(options) {
|
|
21
|
+
this.clients = new SlidingWindow_1.SlidingWindow({
|
|
22
|
+
windowMs: PasswordResetThrottle.CLIENT_WINDOW_MS,
|
|
23
|
+
limit: PasswordResetThrottle.CLIENT_LIMIT,
|
|
24
|
+
now: options === null || options === void 0 ? void 0 : options.now,
|
|
25
|
+
});
|
|
26
|
+
this.accounts = new SlidingWindow_1.SlidingWindow({
|
|
27
|
+
windowMs: PasswordResetThrottle.ACCOUNT_WINDOW_MS,
|
|
28
|
+
limit: PasswordResetThrottle.ACCOUNT_LIMIT,
|
|
29
|
+
now: options === null || options === void 0 ? void 0 : options.now,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Count this request and answer which window refuses it, if any. */
|
|
33
|
+
PasswordResetThrottle.prototype.admit = function (client, account) {
|
|
34
|
+
if (this.clients.hit(client)) {
|
|
35
|
+
return 'client';
|
|
36
|
+
}
|
|
37
|
+
if (this.accounts.hit(account)) {
|
|
38
|
+
return 'account';
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Per client: 10 requests an hour. Someone asking for their own address again, or a household
|
|
44
|
+
* asking for a few addresses, passes; a script sweeping addresses stops at ten.
|
|
45
|
+
*/
|
|
46
|
+
PasswordResetThrottle.CLIENT_LIMIT = 10;
|
|
47
|
+
PasswordResetThrottle.CLIENT_WINDOW_MS = 60 * 60 * 1000;
|
|
48
|
+
/**
|
|
49
|
+
* Per address: 3 requests an hour. A person whose mail is slow asks once or twice more; beyond
|
|
50
|
+
* that it is mail aimed at somebody's inbox.
|
|
51
|
+
*/
|
|
52
|
+
PasswordResetThrottle.ACCOUNT_LIMIT = 3;
|
|
53
|
+
PasswordResetThrottle.ACCOUNT_WINDOW_MS = 60 * 60 * 1000;
|
|
54
|
+
return PasswordResetThrottle;
|
|
55
|
+
}());
|
|
56
|
+
exports.PasswordResetThrottle = PasswordResetThrottle;
|
|
57
|
+
/** The process-wide windows the reset door shares — they must span requests. */
|
|
58
|
+
exports.passwordResetThrottle = new PasswordResetThrottle();
|
|
59
|
+
//# sourceMappingURL=PasswordResetThrottle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PasswordResetThrottle.js","sourceRoot":"","sources":["../../../src/throttle/PasswordResetThrottle.ts"],"names":[],"mappings":";;;AAAA,iDAAgD;AAGhD;;;;;;;;;;;;;GAaG;AACH;IAkBE,+BAAY,OAAgC;QAC1C,IAAI,CAAC,OAAO,GAAG,IAAI,6BAAa,CAAC;YAC/B,QAAQ,EAAE,qBAAqB,CAAC,gBAAgB;YAChD,KAAK,EAAE,qBAAqB,CAAC,YAAY;YACzC,GAAG,EAAE,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,GAAG;SAClB,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,GAAG,IAAI,6BAAa,CAAC;YAChC,QAAQ,EAAE,qBAAqB,CAAC,iBAAiB;YACjD,KAAK,EAAE,qBAAqB,CAAC,aAAa;YAC1C,GAAG,EAAE,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,GAAG;SAClB,CAAC,CAAC;IACL,CAAC;IAED,qEAAqE;IACrE,qCAAK,GAAL,UAAM,MAAc,EAAE,OAAe;QACnC,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;YAC5B,OAAO,QAAQ,CAAC;SACjB;QACD,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE;YAC9B,OAAO,SAAS,CAAC;SAClB;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IAvCD;;;OAGG;IACqB,kCAAY,GAAG,EAAE,CAAC;IAClB,sCAAgB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAE1D;;;OAGG;IACqB,mCAAa,GAAG,CAAC,CAAC;IAClB,uCAAiB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IA4B7D,4BAAC;CAAA,AAzCD,IAyCC;AAzCY,sDAAqB;AA2ClC,gFAAgF;AACnE,QAAA,qBAAqB,GAAG,IAAI,qBAAqB,EAAE,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two keyed digests the throttled doors key their windows on and write on their log lines,
|
|
3
|
+
* in place of what they stand for:
|
|
4
|
+
* - `account(email)` — the ACCOUNT DIGEST: a keyed hash of the trimmed, lowercased address.
|
|
5
|
+
* One address gives one digest however it was typed, on every replica and across restarts, so
|
|
6
|
+
* an operator can say "one account" across lines without the address.
|
|
7
|
+
* - `coarseIp(address)` — the COARSE IP HASH: a keyed hash of the client address at the grain
|
|
8
|
+
* one device holds (an IPv4 address; an IPv6 /64, inside which a device rotates freely), so
|
|
9
|
+
* an operator can say "one device" and a throttle cannot be dodged by rotating inside a /64.
|
|
10
|
+
*
|
|
11
|
+
* Keyed with HMAC-SHA256 under a key DERIVED from the session secret — the one secret every
|
|
12
|
+
* replica already shares, the same keying the invite-request door's IP hash uses; derived per
|
|
13
|
+
* purpose, never used raw, so neither digest weakens the session's own use of it, and the two
|
|
14
|
+
* digests never collide in meaning. Truncated to 64 bits: enough to tell accounts and devices
|
|
15
|
+
* apart, useless to anyone without the key — never the address, never a plain hash a list of
|
|
16
|
+
* addresses could reverse.
|
|
17
|
+
*
|
|
18
|
+
* The key is the deployment's `SESSION_SECRET` unless the constructor is given one (the tests).
|
|
19
|
+
* Without either the digests refuse to run: a server with no `SESSION_SECRET` has no sessions
|
|
20
|
+
* either (the session middleware refuses to start), so nothing ever runs unkeyed — never a plain
|
|
21
|
+
* hash a list of addresses could reverse, never a per-process key that quietly stops matching
|
|
22
|
+
* across replicas.
|
|
23
|
+
*/
|
|
24
|
+
export declare class RequestDigests {
|
|
25
|
+
private readonly options?;
|
|
26
|
+
/** Hex characters kept from the HMAC: 64 bits. */
|
|
27
|
+
private static readonly DIGEST_HEX_LENGTH;
|
|
28
|
+
constructor(options?: {
|
|
29
|
+
secret?: string | undefined;
|
|
30
|
+
} | undefined);
|
|
31
|
+
/** The account digest of an address, however it was typed. */
|
|
32
|
+
account(email: string): string;
|
|
33
|
+
/** The coarse IP hash of a client address (see `ClientAddress`). */
|
|
34
|
+
coarseIp(address: string): string;
|
|
35
|
+
private digest;
|
|
36
|
+
private secret;
|
|
37
|
+
/** An IPv4 address as itself (an IPv4-mapped IPv6 address as its IPv4); an IPv6 address as its /64. */
|
|
38
|
+
private coarsen;
|
|
39
|
+
/** The first four groups (the /64) of an IPv6 address, each without leading zeros; undefined when it does not parse. */
|
|
40
|
+
private ipv6Groups;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=RequestDigests.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RequestDigests.d.ts","sourceRoot":"","sources":["../../../src/throttle/RequestDigests.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,cAAc;IAIb,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAHrC,kDAAkD;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAM;gBAElB,OAAO,CAAC;;iBAAqB;IAE1D,8DAA8D;IAC9D,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAI9B,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAIjC,OAAO,CAAC,MAAM;IAOd,OAAO,CAAC,MAAM;IAUd,uGAAuG;IACvG,OAAO,CAAC,OAAO;IAYf,wHAAwH;IACxH,OAAO,CAAC,UAAU;CAwBnB"}
|