@proteinjs/user-server 1.22.0 → 1.22.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/generated/index.js +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/authentication/PasswordResetToken.d.ts +43 -12
  6. package/dist/src/authentication/PasswordResetToken.d.ts.map +1 -1
  7. package/dist/src/authentication/PasswordResetToken.js +86 -21
  8. package/dist/src/authentication/PasswordResetToken.js.map +1 -1
  9. package/dist/src/routes/devLogin.d.ts +1 -1
  10. package/dist/src/routes/devLogin.js +1 -1
  11. package/dist/src/routes/executePasswordReset.d.ts +2 -1
  12. package/dist/src/routes/executePasswordReset.d.ts.map +1 -1
  13. package/dist/src/routes/executePasswordReset.js +9 -7
  14. package/dist/src/routes/executePasswordReset.js.map +1 -1
  15. package/dist/src/routes/initiatePasswordReset.d.ts +3 -2
  16. package/dist/src/routes/initiatePasswordReset.d.ts.map +1 -1
  17. package/dist/src/routes/initiatePasswordReset.js +30 -27
  18. package/dist/src/routes/initiatePasswordReset.js.map +1 -1
  19. package/dist/test/DbTraffic.d.ts +29 -0
  20. package/dist/test/DbTraffic.d.ts.map +1 -0
  21. package/dist/test/DbTraffic.js +106 -0
  22. package/dist/test/DbTraffic.js.map +1 -0
  23. package/dist/test/DevLoginBootstrapAdmin.test.js +3 -4
  24. package/dist/test/DevLoginBootstrapAdmin.test.js.map +1 -1
  25. package/dist/test/DevLoginStaleCookieFirstHit.test.js +1 -1
  26. package/dist/test/DevLoginStaleCookieFirstHit.test.js.map +1 -1
  27. package/dist/test/ExecutePasswordReset.test.js +513 -124
  28. package/dist/test/ExecutePasswordReset.test.js.map +1 -1
  29. package/dist/test/InitiatePasswordReset.test.d.ts +2 -0
  30. package/dist/test/InitiatePasswordReset.test.d.ts.map +1 -0
  31. package/dist/test/InitiatePasswordReset.test.js +415 -0
  32. package/dist/test/InitiatePasswordReset.test.js.map +1 -0
  33. package/dist/test/LogCapture.d.ts +22 -0
  34. package/dist/test/LogCapture.d.ts.map +1 -0
  35. package/dist/test/LogCapture.js +127 -0
  36. package/dist/test/LogCapture.js.map +1 -0
  37. package/dist/test/MalformedResetTokens.d.ts +11 -0
  38. package/dist/test/MalformedResetTokens.d.ts.map +1 -0
  39. package/dist/test/MalformedResetTokens.js +35 -0
  40. package/dist/test/MalformedResetTokens.js.map +1 -0
  41. package/dist/test/ValidateResetToken.test.js +156 -33
  42. package/dist/test/ValidateResetToken.test.js.map +1 -1
  43. package/dist/test/emulatorLock.test.d.ts +2 -0
  44. package/dist/test/emulatorLock.test.d.ts.map +1 -0
  45. package/dist/test/emulatorLock.test.js +32 -0
  46. package/dist/test/emulatorLock.test.js.map +1 -0
  47. package/dist/test/emulatorLockName.test.d.ts +2 -0
  48. package/dist/test/emulatorLockName.test.d.ts.map +1 -0
  49. package/dist/test/emulatorLockName.test.js +36 -0
  50. package/dist/test/emulatorLockName.test.js.map +1 -0
  51. package/generated/index.ts +1 -1
  52. package/index.ts +1 -1
  53. package/jest.config.js +2 -2
  54. package/package.json +3 -3
  55. package/src/authentication/PasswordResetToken.ts +74 -20
  56. package/src/routes/devLogin.ts +1 -1
  57. package/src/routes/executePasswordReset.ts +3 -2
  58. package/src/routes/initiatePasswordReset.ts +20 -17
  59. package/test/DbTraffic.ts +43 -0
  60. package/test/DevLoginBootstrapAdmin.test.ts +3 -4
  61. package/test/DevLoginStaleCookieFirstHit.test.ts +1 -1
  62. package/test/ExecutePasswordReset.test.ts +308 -70
  63. package/test/InitiatePasswordReset.test.ts +250 -0
  64. package/test/LogCapture.ts +54 -0
  65. package/test/MalformedResetTokens.ts +27 -0
  66. package/test/ValidateResetToken.test.ts +108 -24
  67. package/test/emulatorLock.js +5 -6
  68. package/test/emulatorLock.test.ts +32 -0
  69. package/test/emulatorLockName.test.ts +33 -0
@@ -1 +1 @@
1
- {"version":3,"file":"PasswordResetToken.d.ts","sourceRoot":"","sources":["../../../src/authentication/PasswordResetToken.ts"],"names":[],"mappings":"AAGA,OAAO,EAAU,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE/C,MAAM,MAAM,uBAAuB,GAC/B;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GACjC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAEjD,IAAI,IAAI,MAAM;IAIR,OAAO,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAkBnE;;;;OAIG;IACG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASjF,8GAA8G;IAC9G,WAAW,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS;IASnD,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,MAAM;CAQf"}
1
+ {"version":3,"file":"PasswordResetToken.d.ts","sourceRoot":"","sources":["../../../src/authentication/PasswordResetToken.ts"],"names":[],"mappings":"AACA,OAAe,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,EAAU,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE/C,MAAM,MAAM,uBAAuB,GAC/B;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GACjC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAoB;IACjD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAM;IAE9C;;;OAGG;IACG,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;IAUjC,OAAO,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAmBnE;;;;OAIG;IACG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASjF;;;OAGG;IACG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQtD,+FAA+F;IAC/F,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS;IAQxC;;;;;;;OAOG;IACH,WAAW,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS;IASnD,OAAO,CAAC,KAAK;IAIb,yEAAyE;IACzE,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,MAAM;CAQf"}
@@ -48,26 +48,55 @@ var user_1 = require("@proteinjs/user");
48
48
  * The password-reset token's one owner: its shape, its mint, the lookup that maps a presented
49
49
  * value back to the user it was issued to, and its single-use redemption.
50
50
  *
51
- * A token is 32 bytes from the platform CSPRNG, hex-encoded (64 lowercase hex characters),
52
- * stored on the user row beside its expiry until it is redeemed or expires.
51
+ * A token is 32 bytes from the platform CSPRNG, hex-encoded (64 lowercase hex characters). The
52
+ * token itself exists only in the link mailed to the account: the user row stores its SHA-256
53
+ * digest (hex) beside the expiry, never the token, so a read of the table yields nothing that
54
+ * can be presented. A digest is not a token — presenting one is hashed again and matches no row.
55
+ * A fast unsalted hash is enough here because the input is 256 bits of CSPRNG output, not a
56
+ * human-chosen secret.
53
57
  *
54
- * Resolution refuses anything that is not a value of that shape BEFORE any lookup. A query
55
- * filter built from a request value that is not a token does not compare the way a token does:
56
- * `null` renders as `IS NULL` and would match every account with no pending reset, an empty
57
- * string matches an emptied column, and other types reach the driver. The row the lookup
58
- * returns is then re-checked in code — its stored token must be a string equal to the presented
59
- * one (compared in constant time) and its expiry a real timestamp still in the future — so the
58
+ * Resolution refuses anything that is not a value of the token's shape BEFORE any lookup. A
59
+ * query filter built from a request value that is not a token does not compare the way a token
60
+ * does: `null` renders as `IS NULL` and would match every account with no pending reset, an
61
+ * empty string matches an emptied column, and other types reach the driver. The presented token
62
+ * is then hashed, the row is looked up by that digest, and the row the lookup returns is
63
+ * re-checked in code — its stored digest must be a string equal to the presented token's digest
64
+ * (compared in constant time) and its expiry a real timestamp still in the future — so the
60
65
  * outcome never rests on how the storage compares.
66
+ *
67
+ * No migration accompanies the move from a stored token to a stored digest: a row that still
68
+ * holds a token minted before it never equals the digest of anything presented, so that token
69
+ * fails validation for what remains of its hour and the person asks for a new link.
61
70
  */
62
71
  var PasswordResetToken = /** @class */ (function () {
63
72
  function PasswordResetToken() {
64
73
  }
65
- PasswordResetToken.prototype.mint = function () {
66
- return (0, crypto_1.randomBytes)(32).toString('hex');
74
+ /**
75
+ * Issue a token to `user`: store its digest and its expiry on the row, replacing any token
76
+ * outstanding, and return the token — the only time it exists outside the mailed link.
77
+ */
78
+ PasswordResetToken.prototype.mint = function (user) {
79
+ return __awaiter(this, void 0, void 0, function () {
80
+ var token;
81
+ return __generator(this, function (_a) {
82
+ switch (_a.label) {
83
+ case 0:
84
+ token = (0, crypto_1.randomBytes)(32).toString('hex');
85
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, {
86
+ id: user.id,
87
+ passwordResetToken: this.digest(token),
88
+ passwordResetTokenExpiration: (0, moment_1.default)().add(PasswordResetToken.LIFETIME_MINUTES, 'minutes'),
89
+ })];
90
+ case 1:
91
+ _a.sent();
92
+ return [2 /*return*/, token];
93
+ }
94
+ });
95
+ });
67
96
  };
68
97
  PasswordResetToken.prototype.resolve = function (presented) {
69
98
  return __awaiter(this, void 0, void 0, function () {
70
- var token, user;
99
+ var token, digest, user;
71
100
  return __generator(this, function (_a) {
72
101
  switch (_a.label) {
73
102
  case 0:
@@ -75,10 +104,11 @@ var PasswordResetToken = /** @class */ (function () {
75
104
  if (token === undefined) {
76
105
  return [2 /*return*/, { status: 'malformed' }];
77
106
  }
78
- return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(user_1.tables.User, { passwordResetToken: token })];
107
+ digest = this.digest(token);
108
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(user_1.tables.User, { passwordResetToken: digest })];
79
109
  case 1:
80
110
  user = _a.sent();
81
- if (!user || !this.matches(user.passwordResetToken, token)) {
111
+ if (!user || !this.matches(user.passwordResetToken, digest)) {
82
112
  return [2 /*return*/, { status: 'unknown' }];
83
113
  }
84
114
  if (!this.isLive(user.passwordResetTokenExpiration)) {
@@ -91,15 +121,15 @@ var PasswordResetToken = /** @class */ (function () {
91
121
  };
92
122
  /**
93
123
  * Write the new credential and clear the token in one conditional update: the row must still
94
- * carry this token at write time, so two presentations of the same token cannot both succeed.
95
- * Resolves false when the token was already redeemed.
124
+ * carry this token's digest at write time, so two presentations of the same token cannot both
125
+ * succeed. Resolves false when the token was already redeemed.
96
126
  */
97
127
  PasswordResetToken.prototype.redeem = function (user, token, hashedPassword) {
98
128
  return __awaiter(this, void 0, void 0, function () {
99
129
  var updated;
100
130
  return __generator(this, function (_a) {
101
131
  switch (_a.label) {
102
- case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, { password: hashedPassword, passwordResetToken: null, passwordResetTokenExpiration: null }, { id: user.id, passwordResetToken: token })];
132
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, { password: hashedPassword, passwordResetToken: null, passwordResetTokenExpiration: null }, { id: user.id, passwordResetToken: this.digest(token) })];
103
133
  case 1:
104
134
  updated = _a.sent();
105
135
  return [2 /*return*/, updated === 1];
@@ -107,22 +137,56 @@ var PasswordResetToken = /** @class */ (function () {
107
137
  });
108
138
  });
109
139
  };
110
- /** A log-safe reference to a presented token: a short digest of a well-formed one, never the value itself. */
140
+ /**
141
+ * Withdraw a token that never reached its owner (the mail carrying it failed to send): the row
142
+ * is cleared only while it still carries this token's digest, so a newer token is left alone.
143
+ */
144
+ PasswordResetToken.prototype.revoke = function (user, token) {
145
+ return __awaiter(this, void 0, void 0, function () {
146
+ return __generator(this, function (_a) {
147
+ switch (_a.label) {
148
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, { passwordResetToken: null, passwordResetTokenExpiration: null }, { id: user.id, passwordResetToken: this.digest(token) })];
149
+ case 1:
150
+ _a.sent();
151
+ return [2 /*return*/];
152
+ }
153
+ });
154
+ });
155
+ };
156
+ /** When the token outstanding on `user` was minted, or undefined when the row carries none. */
157
+ PasswordResetToken.prototype.mintedAt = function (user) {
158
+ if (!user.passwordResetToken || !user.passwordResetTokenExpiration) {
159
+ return undefined;
160
+ }
161
+ return (0, moment_1.default)(user.passwordResetTokenExpiration).subtract(PasswordResetToken.LIFETIME_MINUTES, 'minutes');
162
+ };
163
+ /**
164
+ * A log-safe reference to a presented token: a short prefix of a well-formed one's digest,
165
+ * never the value itself and never a part of it. The routes log it only for a token that
166
+ * resolved `unknown` — one no row carries — so it never lines up with a digest a row holds.
167
+ * What it is for is telling refusals apart in the log: one stale link presented again reads
168
+ * the same each time, a run of different values reads differently. 48 bits of the SHA-256 of
169
+ * 256 random bits identify nothing and can be presented nowhere.
170
+ */
111
171
  PasswordResetToken.prototype.fingerprint = function (presented) {
112
172
  var token = this.parse(presented);
113
173
  if (token === undefined) {
114
174
  return undefined;
115
175
  }
116
- return (0, crypto_1.createHash)('sha256').update(token).digest('hex').slice(0, 12);
176
+ return this.digest(token).slice(0, 12);
117
177
  };
118
178
  PasswordResetToken.prototype.parse = function (presented) {
119
179
  return typeof presented === 'string' && PasswordResetToken.SHAPE.test(presented) ? presented : undefined;
120
180
  };
121
- PasswordResetToken.prototype.matches = function (stored, presented) {
122
- if (typeof stored !== 'string' || stored.length !== presented.length) {
181
+ /** What the row stores in place of a token: its SHA-256, hex-encoded. */
182
+ PasswordResetToken.prototype.digest = function (token) {
183
+ return (0, crypto_1.createHash)('sha256').update(token).digest('hex');
184
+ };
185
+ PasswordResetToken.prototype.matches = function (storedDigest, presentedDigest) {
186
+ if (typeof storedDigest !== 'string' || storedDigest.length !== presentedDigest.length) {
123
187
  return false;
124
188
  }
125
- return (0, crypto_1.timingSafeEqual)(Buffer.from(stored), Buffer.from(presented));
189
+ return (0, crypto_1.timingSafeEqual)(Buffer.from(storedDigest), Buffer.from(presentedDigest));
126
190
  };
127
191
  PasswordResetToken.prototype.isLive = function (expiration) {
128
192
  if (!expiration) {
@@ -132,6 +196,7 @@ var PasswordResetToken = /** @class */ (function () {
132
196
  return expiresAt.isValid() && (0, moment_1.default)().isBefore(expiresAt);
133
197
  };
134
198
  PasswordResetToken.SHAPE = /^[0-9a-f]{64}$/;
199
+ PasswordResetToken.LIFETIME_MINUTES = 60;
135
200
  return PasswordResetToken;
136
201
  }());
137
202
  exports.PasswordResetToken = PasswordResetToken;
@@ -1 +1 @@
1
- {"version":3,"file":"PasswordResetToken.js","sourceRoot":"","sources":["../../../src/authentication/PasswordResetToken.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PasswordResetToken.js","sourceRoot":"","sources":["../../../src/authentication/PasswordResetToken.ts"],"names":[],"mappings":"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"}
@@ -32,7 +32,7 @@ import { Route } from '@proteinjs/server-api';
32
32
  * call is unchanged; the variable absent = nothing changes; the gates closed = 404 regardless.
33
33
  * Test and prod never set it — the omission is the safety, the same idiom as the gates. The
34
34
  * outcome is logged as ONE marker line, `Dev bootstrap admin door: <granted|admin-exists>`,
35
- * which the n3xa compose-estate boot proof reads from the server log to PROVE the grant landed.
35
+ * which a consumer's boot proof can read from the server log to PROVE the grant landed.
36
36
  *
37
37
  * Role-bootstrap door (`DEV_BOOTSTRAP_ROLES='email:role[,role];email:role…'`, the grammar in
38
38
  * DevBootstrapRoles.ts): the first-admin door leaves every OTHER account role-less, and a
@@ -80,7 +80,7 @@ var emailDomain = function (address) { return address.slice(address.lastIndexOf(
80
80
  * call is unchanged; the variable absent = nothing changes; the gates closed = 404 regardless.
81
81
  * Test and prod never set it — the omission is the safety, the same idiom as the gates. The
82
82
  * outcome is logged as ONE marker line, `Dev bootstrap admin door: <granted|admin-exists>`,
83
- * which the n3xa compose-estate boot proof reads from the server log to PROVE the grant landed.
83
+ * which a consumer's boot proof can read from the server log to PROVE the grant landed.
84
84
  *
85
85
  * Role-bootstrap door (`DEV_BOOTSTRAP_ROLES='email:role[,role];email:role…'`, the grammar in
86
86
  * DevBootstrapRoles.ts): the first-admin door leaves every OTHER account role-less, and a
@@ -5,7 +5,8 @@ import { Route } from '@proteinjs/server-api';
5
5
  * Resolves the presented token through `PasswordResetToken` — which refuses anything but a
6
6
  * well-formed token before any lookup — checks its expiry, and redeems it: the new password is
7
7
  * written and the token cleared in one conditional update, so a token resets a password once.
8
- * The token itself never reaches the log.
8
+ * The token itself never reaches the log. A request that carries no body at all is refused like
9
+ * one that carries a blank password.
9
10
  *
10
11
  * @bodyParam {string} token - The password reset token.
11
12
  * @bodyParam {string} newPassword - The new password for the user.
@@ -1 +1 @@
1
- {"version":3,"file":"executePasswordReset.d.ts","sourceRoot":"","sources":["../../../src/routes/executePasswordReset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAM9C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,EAAE,KAuClC,CAAC"}
1
+ {"version":3,"file":"executePasswordReset.d.ts","sourceRoot":"","sources":["../../../src/routes/executePasswordReset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAM9C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,EAAE,KAuClC,CAAC"}
@@ -47,7 +47,8 @@ var PasswordResetToken_1 = require("../authentication/PasswordResetToken");
47
47
  * Resolves the presented token through `PasswordResetToken` — which refuses anything but a
48
48
  * well-formed token before any lookup — checks its expiry, and redeems it: the new password is
49
49
  * written and the token cleared in one conditional update, so a token resets a password once.
50
- * The token itself never reaches the log.
50
+ * The token itself never reaches the log. A request that carries no body at all is refused like
51
+ * one that carries a blank password.
51
52
  *
52
53
  * @bodyParam {string} token - The password reset token.
53
54
  * @bodyParam {string} newPassword - The new password for the user.
@@ -57,11 +58,12 @@ exports.executePasswordReset = {
57
58
  method: user_1.routes.executePasswordReset.method,
58
59
  onRequest: function (request, response) { return __awaiter(void 0, void 0, void 0, function () {
59
60
  var logger, _a, token, newPassword, resetToken, resolution, user, hashedPassword;
60
- return __generator(this, function (_b) {
61
- switch (_b.label) {
61
+ var _b;
62
+ return __generator(this, function (_c) {
63
+ switch (_c.label) {
62
64
  case 0:
63
65
  logger = new logger_1.Logger({ name: 'executePasswordReset' });
64
- _a = request.body, token = _a.token, newPassword = _a.newPassword;
66
+ _a = (_b = request.body) !== null && _b !== void 0 ? _b : {}, token = _a.token, newPassword = _a.newPassword;
65
67
  if (typeof newPassword !== 'string' || newPassword.length === 0) {
66
68
  response.status(400).send({ error: 'New password cannot be blank' });
67
69
  return [2 /*return*/];
@@ -69,7 +71,7 @@ exports.executePasswordReset = {
69
71
  resetToken = new PasswordResetToken_1.PasswordResetToken();
70
72
  return [4 /*yield*/, resetToken.resolve(token)];
71
73
  case 1:
72
- resolution = _b.sent();
74
+ resolution = _c.sent();
73
75
  if (resolution.status === 'malformed' || resolution.status === 'unknown') {
74
76
  logger.info({
75
77
  message: "Invalid reset token used",
@@ -86,10 +88,10 @@ exports.executePasswordReset = {
86
88
  user = resolution.user;
87
89
  return [4 /*yield*/, new PasswordHasher_1.PasswordHasher().hash(newPassword)];
88
90
  case 2:
89
- hashedPassword = _b.sent();
91
+ hashedPassword = _c.sent();
90
92
  return [4 /*yield*/, resetToken.redeem(user, resolution.token, hashedPassword)];
91
93
  case 3:
92
- if (!(_b.sent())) {
94
+ if (!(_c.sent())) {
93
95
  logger.info({ message: "Reset token already redeemed", obj: { email: user.email } });
94
96
  response.status(400).send({ error: 'Invalid or expired reset token' });
95
97
  return [2 /*return*/];
@@ -1 +1 @@
1
- {"version":3,"file":"executePasswordReset.js","sourceRoot":"","sources":["../../../src/routes/executePasswordReset.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,wCAAyC;AACzC,4CAA2C;AAC3C,mEAAkE;AAClE,2EAA0E;AAE1E;;;;;;;;;;GAUG;AACU,QAAA,oBAAoB,GAAU;IACzC,IAAI,EAAE,aAAM,CAAC,oBAAoB,CAAC,IAAI;IACtC,MAAM,EAAE,aAAM,CAAC,oBAAoB,CAAC,MAAM;IAC1C,SAAS,EAAE,UAAO,OAAO,EAAE,QAAQ;;;;;oBAC3B,MAAM,GAAG,IAAI,eAAM,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,CAAC,CAAC;oBACtD,KAAyB,OAAO,CAAC,IAAI,EAAnC,KAAK,WAAA,EAAE,WAAW,iBAAA,CAAkB;oBAC5C,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE;wBAC/D,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAC,CAAC;wBACrE,sBAAO;qBACR;oBAEK,UAAU,GAAG,IAAI,uCAAkB,EAAE,CAAC;oBACzB,qBAAM,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,EAAA;;oBAA5C,UAAU,GAAG,SAA+B;oBAClD,IAAI,UAAU,CAAC,MAAM,KAAK,WAAW,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS,EAAE;wBACxE,MAAM,CAAC,IAAI,CAAC;4BACV,OAAO,EAAE,0BAA0B;4BACnC,GAAG,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE;yBACzE,CAAC,CAAC;wBACH,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC,CAAC;wBACvE,sBAAO;qBACR;oBAED,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS,EAAE;wBACnC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,0BAA0B,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;wBAC5F,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC,CAAC;wBAChE,sBAAO;qBACR;oBAEO,IAAI,GAAK,UAAU,KAAf,CAAgB;oBACL,qBAAM,IAAI,+BAAc,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,EAAA;;oBAA7D,cAAc,GAAG,SAA4C;oBAC7D,qBAAM,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,EAAE,cAAc,CAAC,EAAA;;oBAArE,IAAI,CAAC,CAAC,SAA+D,CAAC,EAAE;wBACtE,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,8BAA8B,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;wBACrF,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC,CAAC;wBACvE,sBAAO;qBACR;oBAED,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,6BAA6B,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;oBACpF,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,sCAAsC,EAAE,CAAC,CAAC;;;;SACpE;CACF,CAAC"}
1
+ {"version":3,"file":"executePasswordReset.js","sourceRoot":"","sources":["../../../src/routes/executePasswordReset.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,wCAAyC;AACzC,4CAA2C;AAC3C,mEAAkE;AAClE,2EAA0E;AAE1E;;;;;;;;;;;GAWG;AACU,QAAA,oBAAoB,GAAU;IACzC,IAAI,EAAE,aAAM,CAAC,oBAAoB,CAAC,IAAI;IACtC,MAAM,EAAE,aAAM,CAAC,oBAAoB,CAAC,MAAM;IAC1C,SAAS,EAAE,UAAO,OAAO,EAAE,QAAQ;;;;;;oBAC3B,MAAM,GAAG,IAAI,eAAM,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,CAAC,CAAC;oBACtD,KAAyB,MAAA,OAAO,CAAC,IAAI,mCAAI,EAAE,EAAzC,KAAK,WAAA,EAAE,WAAW,iBAAA,CAAwB;oBAClD,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE;wBAC/D,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAC,CAAC;wBACrE,sBAAO;qBACR;oBAEK,UAAU,GAAG,IAAI,uCAAkB,EAAE,CAAC;oBACzB,qBAAM,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,EAAA;;oBAA5C,UAAU,GAAG,SAA+B;oBAClD,IAAI,UAAU,CAAC,MAAM,KAAK,WAAW,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS,EAAE;wBACxE,MAAM,CAAC,IAAI,CAAC;4BACV,OAAO,EAAE,0BAA0B;4BACnC,GAAG,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE;yBACzE,CAAC,CAAC;wBACH,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC,CAAC;wBACvE,sBAAO;qBACR;oBAED,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS,EAAE;wBACnC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,0BAA0B,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;wBAC5F,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC,CAAC;wBAChE,sBAAO;qBACR;oBAEO,IAAI,GAAK,UAAU,KAAf,CAAgB;oBACL,qBAAM,IAAI,+BAAc,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,EAAA;;oBAA7D,cAAc,GAAG,SAA4C;oBAC7D,qBAAM,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,EAAE,cAAc,CAAC,EAAA;;oBAArE,IAAI,CAAC,CAAC,SAA+D,CAAC,EAAE;wBACtE,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,8BAA8B,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;wBACrF,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC,CAAC;wBACvE,sBAAO;qBACR;oBAED,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,6BAA6B,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;oBACpF,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,sCAAsC,EAAE,CAAC,CAAC;;;;SACpE;CACF,CAAC"}
@@ -2,8 +2,9 @@ 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.
7
8
  *
8
9
  * Requires an implementation of `DefaultPasswordResetEmailConfigFactory` to build the password reset email with.
9
10
  *
@@ -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;AAW9C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAkEnC,CAAC"}
@@ -60,8 +60,9 @@ var PasswordResetToken_1 = require("../authentication/PasswordResetToken");
60
60
  /**
61
61
  * Route for initiating a password reset process.
62
62
  *
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.
63
+ * Mints a reset token through `PasswordResetToken` — which stores only the token's digest and
64
+ * its expiry on the user row — and mails the token to the account as a reset link. The token is
65
+ * withdrawn again when the mail fails to send.
65
66
  *
66
67
  * Requires an implementation of `DefaultPasswordResetEmailConfigFactory` to build the password reset email with.
67
68
  *
@@ -74,13 +75,18 @@ exports.initiatePasswordReset = {
74
75
  path: user_1.routes.initiatePasswordReset.path,
75
76
  method: user_1.routes.initiatePasswordReset.method,
76
77
  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;
78
+ var logger, requestedEmail, email, db, genericResponse, user, resetToken, mintedAt, emailSender, defaultConfigFactory, passwordResetToken, config, _a, text, html, error_1;
78
79
  var _b, _c;
79
80
  return __generator(this, function (_d) {
80
81
  switch (_d.label) {
81
82
  case 0:
82
83
  logger = new logger_1.Logger({ name: 'initiatePasswordReset' });
83
- email = (_b = request.body.email) === null || _b === void 0 ? void 0 : _b.toLowerCase();
84
+ requestedEmail = ((_b = request.body) !== null && _b !== void 0 ? _b : {}).email;
85
+ if (typeof requestedEmail !== 'string' || requestedEmail.length === 0) {
86
+ response.status(400).send({ error: 'Email cannot be blank' });
87
+ return [2 /*return*/];
88
+ }
89
+ email = requestedEmail.toLowerCase();
84
90
  db = (0, db_1.getDbAsSystem)();
85
91
  genericResponse = { message: 'If an account with that email exists, we have sent a password reset link.' };
86
92
  return [4 /*yield*/, db.get(user_1.tables.User, { email: email })];
@@ -92,47 +98,44 @@ exports.initiatePasswordReset = {
92
98
  response.send(genericResponse);
93
99
  return [2 /*return*/];
94
100
  }
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
- }
101
+ resetToken = new PasswordResetToken_1.PasswordResetToken();
102
+ mintedAt = resetToken.mintedAt(user);
103
+ if (mintedAt && (0, moment_1.default)().diff(mintedAt, 'minutes') < 5) {
104
+ logger.info({ message: "Password reset requested too soon for user", obj: { email: email } });
105
+ response.send(genericResponse);
106
+ return [2 /*return*/];
105
107
  }
106
108
  emailSender = new email_server_1.EmailSender();
107
109
  defaultConfigFactory = (0, email_server_1.getDefaultPasswordResetEmailConfigFactory)();
108
110
  if (!defaultConfigFactory) {
109
111
  throw new Error("Unable to find a @proteinjs/email-server/DefaultPasswordResetEmailConfigFactory implementation when initiating password reset.");
110
112
  }
111
- passwordResetToken = new PasswordResetToken_1.PasswordResetToken().mint();
112
- passwordResetTokenExpiration = (0, moment_1.default)().add(1, 'hour');
113
- _d.label = 2;
113
+ return [4 /*yield*/, resetToken.mint(user)];
114
114
  case 2:
115
- _d.trys.push([2, 5, , 6]);
115
+ passwordResetToken = _d.sent();
116
+ _d.label = 3;
117
+ case 3:
118
+ _d.trys.push([3, 5, , 7]);
116
119
  config = defaultConfigFactory.getConfig();
117
120
  _a = config.getEmailContent("".concat(user_1.uiRoutes.auth.passwordReset, "?token=").concat(passwordResetToken)), text = _a.text, html = _a.html;
118
121
  // Send email containing a reset link
119
122
  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
123
  case 4:
126
- // If email is sent successfully, save reset token to user record
124
+ // Send email containing a reset link
127
125
  _d.sent();
128
126
  response.send(genericResponse);
129
- return [3 /*break*/, 6];
127
+ return [3 /*break*/, 7];
130
128
  case 5:
131
129
  error_1 = _d.sent();
132
130
  logger.error({ message: "Failed to send password reset email", obj: { email: email }, error: error_1 });
131
+ // The link never reached the account: withdraw the token so asking again is not throttled
132
+ return [4 /*yield*/, resetToken.revoke(user, passwordResetToken)];
133
+ case 6:
134
+ // The link never reached the account: withdraw the token so asking again is not throttled
135
+ _d.sent();
133
136
  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*/];
137
+ return [3 /*break*/, 7];
138
+ case 7: return [2 /*return*/];
136
139
  }
137
140
  });
138
141
  }); },
@@ -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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"}
@@ -0,0 +1,29 @@
1
+ import { DbDriver } from '@proteinjs/db';
2
+ /**
3
+ * What reached the database while a run executed — the outcome a "refused before any lookup"
4
+ * claim rests on. A response code cannot prove it: a value that is wrongly let through still
5
+ * matches no row and still answers 400, so the proof is that nothing was built and nothing ran.
6
+ * - `lookups`: the filter of every read built through the package's `Db` — every `query`, which
7
+ * is also what a `get` is (the first row of one);
8
+ * - `writes`: how many writes were built through it (`insert`, `update`, `delete`);
9
+ * - `statements`: how many statements the driver ran, whoever built them.
10
+ */
11
+ export declare class DbTraffic {
12
+ readonly lookups: unknown[];
13
+ readonly writes: number;
14
+ readonly statements: number;
15
+ private constructor();
16
+ /** The traffic of a run that never touched the database: `expect(traffic).toEqual(DbTraffic.NONE)`. */
17
+ static readonly NONE: {
18
+ lookups: never[];
19
+ writes: number;
20
+ statements: number;
21
+ };
22
+ /** Runs `run` and answers what it resolved to beside the database traffic it caused. */
23
+ static during<T>(driver: DbDriver, run: () => Promise<T>): Promise<{
24
+ result: T;
25
+ traffic: DbTraffic;
26
+ }>;
27
+ private static callCount;
28
+ }
29
+ //# sourceMappingURL=DbTraffic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DbTraffic.d.ts","sourceRoot":"","sources":["../../test/DbTraffic.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE7C;;;;;;;;GAQG;AACH,qBAAa,SAAS;IAElB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM;IAH7B,OAAO;IAMP,uGAAuG;IACvG,MAAM,CAAC,QAAQ,CAAC,IAAI;;;;MAA6C;IAEjE,wFAAwF;WAC3E,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,CAAC,CAAC;QAAC,OAAO,EAAE,SAAS,CAAA;KAAE,CAAC;IAiB3G,OAAO,CAAC,MAAM,CAAC,SAAS;CAGzB"}
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
39
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
40
+ if (ar || !(i in from)) {
41
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
42
+ ar[i] = from[i];
43
+ }
44
+ }
45
+ return to.concat(ar || Array.prototype.slice.call(from));
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.DbTraffic = void 0;
49
+ var db_1 = require("@proteinjs/db");
50
+ /**
51
+ * What reached the database while a run executed — the outcome a "refused before any lookup"
52
+ * claim rests on. A response code cannot prove it: a value that is wrongly let through still
53
+ * matches no row and still answers 400, so the proof is that nothing was built and nothing ran.
54
+ * - `lookups`: the filter of every read built through the package's `Db` — every `query`, which
55
+ * is also what a `get` is (the first row of one);
56
+ * - `writes`: how many writes were built through it (`insert`, `update`, `delete`);
57
+ * - `statements`: how many statements the driver ran, whoever built them.
58
+ */
59
+ var DbTraffic = /** @class */ (function () {
60
+ function DbTraffic(lookups, writes, statements) {
61
+ this.lookups = lookups;
62
+ this.writes = writes;
63
+ this.statements = statements;
64
+ }
65
+ /** Runs `run` and answers what it resolved to beside the database traffic it caused. */
66
+ DbTraffic.during = function (driver, run) {
67
+ return __awaiter(this, void 0, void 0, function () {
68
+ var reads, writes, statements, result, lookups;
69
+ return __generator(this, function (_a) {
70
+ switch (_a.label) {
71
+ case 0:
72
+ reads = jest.spyOn(db_1.Db.prototype, 'query');
73
+ writes = [
74
+ jest.spyOn(db_1.Db.prototype, 'insert'),
75
+ jest.spyOn(db_1.Db.prototype, 'update'),
76
+ jest.spyOn(db_1.Db.prototype, 'delete'),
77
+ ];
78
+ statements = [jest.spyOn(driver, 'runQuery'), jest.spyOn(driver, 'runDml')];
79
+ _a.label = 1;
80
+ case 1:
81
+ _a.trys.push([1, , 3, 4]);
82
+ return [4 /*yield*/, run()];
83
+ case 2:
84
+ result = _a.sent();
85
+ lookups = reads.mock.calls.map(function (_a) {
86
+ var _table = _a[0], filter = _a[1];
87
+ return filter;
88
+ });
89
+ return [2 /*return*/, { result: result, traffic: new DbTraffic(lookups, DbTraffic.callCount(writes), DbTraffic.callCount(statements)) }];
90
+ case 3:
91
+ __spreadArray(__spreadArray([reads], writes, true), statements, true).forEach(function (spy) { return spy.mockRestore(); });
92
+ return [7 /*endfinally*/];
93
+ case 4: return [2 /*return*/];
94
+ }
95
+ });
96
+ });
97
+ };
98
+ DbTraffic.callCount = function (spies) {
99
+ return spies.reduce(function (count, spy) { return count + spy.mock.calls.length; }, 0);
100
+ };
101
+ /** The traffic of a run that never touched the database: `expect(traffic).toEqual(DbTraffic.NONE)`. */
102
+ DbTraffic.NONE = { lookups: [], writes: 0, statements: 0 };
103
+ return DbTraffic;
104
+ }());
105
+ exports.DbTraffic = DbTraffic;
106
+ //# sourceMappingURL=DbTraffic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DbTraffic.js","sourceRoot":"","sources":["../../test/DbTraffic.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,oCAA6C;AAE7C;;;;;;;;GAQG;AACH;IACE,mBACW,OAAkB,EAClB,MAAc,EACd,UAAkB;QAFlB,YAAO,GAAP,OAAO,CAAW;QAClB,WAAM,GAAN,MAAM,CAAQ;QACd,eAAU,GAAV,UAAU,CAAQ;IAC1B,CAAC;IAKJ,wFAAwF;IAC3E,gBAAM,GAAnB,UAAuB,MAAgB,EAAE,GAAqB;;;;;;wBACtD,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAE,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;wBAC1C,MAAM,GAAG;4BACb,IAAI,CAAC,KAAK,CAAC,OAAE,CAAC,SAAS,EAAE,QAAQ,CAAC;4BAClC,IAAI,CAAC,KAAK,CAAC,OAAE,CAAC,SAAS,EAAE,QAAQ,CAAC;4BAClC,IAAI,CAAC,KAAK,CAAC,OAAE,CAAC,SAAS,EAAE,QAAQ,CAAC;yBACnC,CAAC;wBACI,UAAU,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;;;;wBAEjE,qBAAM,GAAG,EAAE,EAAA;;wBAApB,MAAM,GAAG,SAAW;wBACpB,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAC,EAAgB;gCAAf,MAAM,QAAA,EAAE,MAAM,QAAA;4BAAM,OAAA,MAAM;wBAAN,CAAM,CAAC,CAAC;wBACnE,sBAAO,EAAE,MAAM,QAAA,EAAE,OAAO,EAAE,IAAI,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,EAAE,EAAC;;wBAEjH,6BAAC,KAAK,GAAK,MAAM,SAAK,UAAU,QAAE,OAAO,CAAC,UAAC,GAAG,IAAK,OAAA,GAAG,CAAC,WAAW,EAAE,EAAjB,CAAiB,CAAC,CAAC;;;;;;KAEzE;IAEc,mBAAS,GAAxB,UAAyB,KAAuC;QAC9D,OAAO,KAAK,CAAC,MAAM,CAAC,UAAC,KAAK,EAAE,GAAG,IAAK,OAAA,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAA7B,CAA6B,EAAE,CAAC,CAAC,CAAC;IACxE,CAAC;IAvBD,uGAAuG;IACvF,cAAI,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IAuBnE,gBAAC;CAAA,AA/BD,IA+BC;AA/BY,8BAAS"}
@@ -42,10 +42,9 @@ var devLoginHarness_1 = require("./devLoginHarness");
42
42
  var UserServerTestEnvironment_1 = require("./UserServerTestEnvironment");
43
43
  var testEnv = new UserServerTestEnvironment_1.UserServerTestEnvironment();
44
44
  /**
45
- * `DEV_BOOTSTRAP_ADMIN_EMAIL` — the first-admin door INSIDE `/dev/login` (n3xa plans/DEV_ESTATES.md
46
- * D3, founder-ruled 2026-09-04). A dev estate on a fresh real database has no privileged account
47
- * to grant from, and the no-raw-DB rule forbids the emulator-era seed scripts there — so the ONE
48
- * sanctioned account door mints break-glass, once:
45
+ * `DEV_BOOTSTRAP_ADMIN_EMAIL` — the first-admin door INSIDE `/dev/login`. A development server on
46
+ * a fresh real database has no privileged account to grant from, and a no-raw-database-write rule
47
+ * forbids seed scripts there — so the ONE sanctioned account door mints break-glass, once:
49
48
  * - behind the door's existing two gates (DEVELOPMENT AND DEV_AUTO_LOGIN_EMAIL): closed = 404 as
50
49
  * before, and the variable changes nothing;
51
50
  * - only while NO account carries 'admin' (the same membership test the app's admin checks make);