@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.
Files changed (133) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/generated/index.js +1 -1
  3. package/dist/generated/index.js.map +1 -1
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +10 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/src/authentication/PasswordHasher.d.ts +9 -0
  9. package/dist/src/authentication/PasswordHasher.d.ts.map +1 -1
  10. package/dist/src/authentication/PasswordHasher.js +25 -0
  11. package/dist/src/authentication/PasswordHasher.js.map +1 -1
  12. package/dist/src/authentication/PasswordResetToken.d.ts +43 -12
  13. package/dist/src/authentication/PasswordResetToken.d.ts.map +1 -1
  14. package/dist/src/authentication/PasswordResetToken.js +86 -21
  15. package/dist/src/authentication/PasswordResetToken.js.map +1 -1
  16. package/dist/src/authentication/authenticate.d.ts +15 -0
  17. package/dist/src/authentication/authenticate.d.ts.map +1 -1
  18. package/dist/src/authentication/authenticate.js +55 -20
  19. package/dist/src/authentication/authenticate.js.map +1 -1
  20. package/dist/src/routes/devLogin.d.ts +1 -1
  21. package/dist/src/routes/devLogin.js +1 -1
  22. package/dist/src/routes/executePasswordReset.d.ts +6 -1
  23. package/dist/src/routes/executePasswordReset.d.ts.map +1 -1
  24. package/dist/src/routes/executePasswordReset.js +16 -7
  25. package/dist/src/routes/executePasswordReset.js.map +1 -1
  26. package/dist/src/routes/initiatePasswordReset.d.ts +10 -2
  27. package/dist/src/routes/initiatePasswordReset.d.ts.map +1 -1
  28. package/dist/src/routes/initiatePasswordReset.js +55 -34
  29. package/dist/src/routes/initiatePasswordReset.js.map +1 -1
  30. package/dist/src/routes/login.d.ts +8 -0
  31. package/dist/src/routes/login.d.ts.map +1 -1
  32. package/dist/src/routes/login.js +63 -22
  33. package/dist/src/routes/login.js.map +1 -1
  34. package/dist/src/throttle/ClientAddress.d.ts +35 -0
  35. package/dist/src/throttle/ClientAddress.d.ts.map +1 -0
  36. package/dist/src/throttle/ClientAddress.js +48 -0
  37. package/dist/src/throttle/ClientAddress.js.map +1 -0
  38. package/dist/src/throttle/PasswordResetThrottle.d.ts +39 -0
  39. package/dist/src/throttle/PasswordResetThrottle.d.ts.map +1 -0
  40. package/dist/src/throttle/PasswordResetThrottle.js +59 -0
  41. package/dist/src/throttle/PasswordResetThrottle.js.map +1 -0
  42. package/dist/src/throttle/RequestDigests.d.ts +42 -0
  43. package/dist/src/throttle/RequestDigests.d.ts.map +1 -0
  44. package/dist/src/throttle/RequestDigests.js +96 -0
  45. package/dist/src/throttle/RequestDigests.js.map +1 -0
  46. package/dist/src/throttle/SignInThrottle.d.ts +67 -0
  47. package/dist/src/throttle/SignInThrottle.d.ts.map +1 -0
  48. package/dist/src/throttle/SignInThrottle.js +91 -0
  49. package/dist/src/throttle/SignInThrottle.js.map +1 -0
  50. package/dist/src/throttle/SlidingWindow.d.ts +51 -0
  51. package/dist/src/throttle/SlidingWindow.d.ts.map +1 -0
  52. package/dist/src/throttle/SlidingWindow.js +85 -0
  53. package/dist/src/throttle/SlidingWindow.js.map +1 -0
  54. package/dist/test/ClientAddress.test.d.ts +2 -0
  55. package/dist/test/ClientAddress.test.d.ts.map +1 -0
  56. package/dist/test/ClientAddress.test.js +51 -0
  57. package/dist/test/ClientAddress.test.js.map +1 -0
  58. package/dist/test/DbTraffic.d.ts +29 -0
  59. package/dist/test/DbTraffic.d.ts.map +1 -0
  60. package/dist/test/DbTraffic.js +106 -0
  61. package/dist/test/DbTraffic.js.map +1 -0
  62. package/dist/test/DevLoginBootstrapAdmin.test.js +3 -4
  63. package/dist/test/DevLoginBootstrapAdmin.test.js.map +1 -1
  64. package/dist/test/DevLoginStaleCookieFirstHit.test.js +1 -1
  65. package/dist/test/DevLoginStaleCookieFirstHit.test.js.map +1 -1
  66. package/dist/test/ExecutePasswordReset.test.js +513 -124
  67. package/dist/test/ExecutePasswordReset.test.js.map +1 -1
  68. package/dist/test/InitiatePasswordReset.test.d.ts +2 -0
  69. package/dist/test/InitiatePasswordReset.test.d.ts.map +1 -0
  70. package/dist/test/InitiatePasswordReset.test.js +783 -0
  71. package/dist/test/InitiatePasswordReset.test.js.map +1 -0
  72. package/dist/test/LogCapture.d.ts +22 -0
  73. package/dist/test/LogCapture.d.ts.map +1 -0
  74. package/dist/test/LogCapture.js +127 -0
  75. package/dist/test/LogCapture.js.map +1 -0
  76. package/dist/test/LoginThrottle.integration.test.d.ts +2 -0
  77. package/dist/test/LoginThrottle.integration.test.d.ts.map +1 -0
  78. package/dist/test/LoginThrottle.integration.test.js +616 -0
  79. package/dist/test/LoginThrottle.integration.test.js.map +1 -0
  80. package/dist/test/MalformedResetTokens.d.ts +11 -0
  81. package/dist/test/MalformedResetTokens.d.ts.map +1 -0
  82. package/dist/test/MalformedResetTokens.js +35 -0
  83. package/dist/test/MalformedResetTokens.js.map +1 -0
  84. package/dist/test/RequestDigests.test.d.ts +2 -0
  85. package/dist/test/RequestDigests.test.d.ts.map +1 -0
  86. package/dist/test/RequestDigests.test.js +73 -0
  87. package/dist/test/RequestDigests.test.js.map +1 -0
  88. package/dist/test/SlidingWindow.test.d.ts +2 -0
  89. package/dist/test/SlidingWindow.test.d.ts.map +1 -0
  90. package/dist/test/SlidingWindow.test.js +72 -0
  91. package/dist/test/SlidingWindow.test.js.map +1 -0
  92. package/dist/test/ValidateResetToken.test.js +156 -33
  93. package/dist/test/ValidateResetToken.test.js.map +1 -1
  94. package/dist/test/emulatorLock.test.d.ts +2 -0
  95. package/dist/test/emulatorLock.test.d.ts.map +1 -0
  96. package/dist/test/emulatorLock.test.js +32 -0
  97. package/dist/test/emulatorLock.test.js.map +1 -0
  98. package/dist/test/emulatorLockName.test.d.ts +2 -0
  99. package/dist/test/emulatorLockName.test.d.ts.map +1 -0
  100. package/dist/test/emulatorLockName.test.js +36 -0
  101. package/dist/test/emulatorLockName.test.js.map +1 -0
  102. package/generated/index.ts +1 -1
  103. package/index.ts +6 -1
  104. package/jest.config.js +2 -2
  105. package/package.json +3 -3
  106. package/src/authentication/PasswordHasher.ts +18 -1
  107. package/src/authentication/PasswordResetToken.ts +74 -20
  108. package/src/authentication/authenticate.ts +36 -9
  109. package/src/routes/devLogin.ts +1 -1
  110. package/src/routes/executePasswordReset.ts +10 -2
  111. package/src/routes/initiatePasswordReset.ts +46 -25
  112. package/src/routes/login.ts +39 -8
  113. package/src/throttle/ClientAddress.ts +47 -0
  114. package/src/throttle/PasswordResetThrottle.ts +62 -0
  115. package/src/throttle/RequestDigests.ts +97 -0
  116. package/src/throttle/SignInThrottle.ts +98 -0
  117. package/src/throttle/SlidingWindow.ts +90 -0
  118. package/test/ClientAddress.test.ts +54 -0
  119. package/test/DbTraffic.ts +43 -0
  120. package/test/DevLoginBootstrapAdmin.test.ts +3 -4
  121. package/test/DevLoginStaleCookieFirstHit.test.ts +1 -1
  122. package/test/ExecutePasswordReset.test.ts +308 -70
  123. package/test/InitiatePasswordReset.test.ts +456 -0
  124. package/test/LogCapture.ts +54 -0
  125. package/test/LoginThrottle.integration.test.ts +339 -0
  126. package/test/MalformedResetTokens.ts +27 -0
  127. package/test/RequestDigests.test.ts +81 -0
  128. package/test/SlidingWindow.test.ts +85 -0
  129. package/test/ValidateResetToken.test.ts +108 -24
  130. package/test/emulatorLock.js +5 -6
  131. package/test/emulatorLock.test.ts +32 -0
  132. package/test/emulatorLockName.test.ts +33 -0
  133. 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
- * This route handles the process of generating a password reset token,
6
- * sending a reset email to the user, and storing the token and expiration of the token in the database.
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;AAW9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAgEnC,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
- * This route handles the process of generating a password reset token,
64
- * sending a reset email to the user, and storing the token and expiration of the token in the database.
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, email, db, genericResponse, user, currentTime, tokenCreationTime, timeDifference, emailSender, defaultConfigFactory, passwordResetToken, passwordResetTokenExpiration, config, _a, text, html, error_1;
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
- email = (_b = request.body.email) === null || _b === void 0 ? void 0 : _b.toLowerCase();
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: { email: email } });
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
- // Check if there's an existing token and it's less than 5 minutes old
96
- if (user.passwordResetToken && user.passwordResetTokenExpiration) {
97
- currentTime = (0, moment_1.default)();
98
- tokenCreationTime = (0, moment_1.default)(user.passwordResetTokenExpiration).subtract(1, 'hour');
99
- timeDifference = currentTime.diff(tokenCreationTime, 'minutes');
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
- passwordResetToken = new PasswordResetToken_1.PasswordResetToken().mint();
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.trys.push([2, 5, , 6]);
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
- // If email is sent successfully, save reset token to user record
143
+ // Send email containing a reset link
127
144
  _d.sent();
128
- response.send(genericResponse);
129
- return [3 /*break*/, 6];
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: { email: email }, error: error_1 });
133
- response.status(500).send({ error: 'Failed to send password reset email. Please try again later.' });
134
- return [3 /*break*/, 6];
135
- case 6: return [2 /*return*/];
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":"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"}
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;AAM9C,eAAO,MAAM,KAAK,EAAE,KA4CnB,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"}
@@ -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
- return __generator(this, function (_a) {
50
- switch (_a.label) {
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
- credentials = request.body;
53
- if (!credentials.email || !credentials.password) {
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
- console.error(error);
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)(credentials.email, credentials.password)];
60
- case 1:
61
- result = _a.sent();
99
+ return [4 /*yield*/, (0, authenticate_1.authenticate)(email, password)];
100
+ case 3:
101
+ result = _b.sent();
62
102
  if (result !== true) {
63
- console.error(result);
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
- _a.label = 2;
68
- case 2:
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
- error_1 = _a.sent();
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 5:
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, credentials.email)];
91
- case 6:
92
- _a.sent();
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,wCAAyC;AACzC,+DAA8D;AAC9D,uEAAsE;AACtE,+DAA8D;AAEjD,QAAA,KAAK,GAAU;IAC1B,IAAI,EAAE,aAAM,CAAC,KAAK,CAAC,IAAI;IACvB,MAAM,EAAE,aAAM,CAAC,KAAK,CAAC,MAAM;IAC3B,SAAS,EAAE,UAAO,OAAY,EAAE,QAAQ;;;;;oBAChC,WAAW,GAAwC,OAAO,CAAC,IAAI,CAAC;oBACtE,IAAI,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE;wBACzC,KAAK,GAAG,oCAAoC,CAAC;wBACnD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;wBACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,OAAA,EAAE,CAAC,CAAC;wBACzB,sBAAO;qBACR;oBAEc,qBAAM,IAAA,2BAAY,EAAC,WAAW,CAAC,KAAK,EAAE,WAAW,CAAC,QAAQ,CAAC,EAAA;;oBAApE,MAAM,GAAG,SAA2D;oBAC1E,IAAI,MAAM,KAAK,IAAI,EAAE;wBACnB,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;wBACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;wBACjC,sBAAO;qBACR;;;;oBAOW,qBAAM,IAAI,iCAAe,EAAE,CAAC,qBAAqB,CAAC,WAAW,CAAC,KAAK,CAAC,EAAA;;oBAA9E,OAAO,GAAG,SAAoE,CAAC;;;;oBAE/E,iFAAiF;oBACjF,iFAAiF;oBACjF,kFAAkF;oBAClF,sCAAsC;oBACtC,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,OAAK,CAAC,CAAC;oBAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,+CAA+C,EAAE,CAAC,CAAC;oBAC1E,sBAAO;;oBAET,IAAI,OAAO,KAAK,SAAS,EAAE;wBACnB,KAAK,GAAG,8DAA8D,CAAC;wBAC7E,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;wBACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,OAAA,EAAE,CAAC,CAAC;wBACzB,sBAAO;qBACR;oBAED,qBAAM,IAAA,mCAAgB,EAAC,OAAO,EAAE,WAAW,CAAC,KAAK,CAAC,EAAA;;oBAAlD,SAAkD,CAAC;oBACnD,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;;;;SACnB;CACF,CAAC"}
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"}