@proteinjs/user-server 1.21.1 → 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 (81) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/generated/index.d.ts.map +1 -1
  3. package/dist/generated/index.js +5 -1
  4. package/dist/generated/index.js.map +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/src/authentication/PasswordResetToken.d.ts +43 -12
  8. package/dist/src/authentication/PasswordResetToken.d.ts.map +1 -1
  9. package/dist/src/authentication/PasswordResetToken.js +86 -21
  10. package/dist/src/authentication/PasswordResetToken.js.map +1 -1
  11. package/dist/src/routes/devLogin.d.ts +1 -1
  12. package/dist/src/routes/devLogin.js +1 -1
  13. package/dist/src/routes/devMail.d.ts +4 -0
  14. package/dist/src/routes/devMail.d.ts.map +1 -0
  15. package/dist/src/routes/devMail.js +119 -0
  16. package/dist/src/routes/devMail.js.map +1 -0
  17. package/dist/src/routes/executePasswordReset.d.ts +2 -1
  18. package/dist/src/routes/executePasswordReset.d.ts.map +1 -1
  19. package/dist/src/routes/executePasswordReset.js +9 -7
  20. package/dist/src/routes/executePasswordReset.js.map +1 -1
  21. package/dist/src/routes/initiatePasswordReset.d.ts +3 -2
  22. package/dist/src/routes/initiatePasswordReset.d.ts.map +1 -1
  23. package/dist/src/routes/initiatePasswordReset.js +30 -27
  24. package/dist/src/routes/initiatePasswordReset.js.map +1 -1
  25. package/dist/test/DbTraffic.d.ts +29 -0
  26. package/dist/test/DbTraffic.d.ts.map +1 -0
  27. package/dist/test/DbTraffic.js +106 -0
  28. package/dist/test/DbTraffic.js.map +1 -0
  29. package/dist/test/DevLoginBootstrapAdmin.test.js +3 -4
  30. package/dist/test/DevLoginBootstrapAdmin.test.js.map +1 -1
  31. package/dist/test/DevLoginStaleCookieFirstHit.test.js +1 -1
  32. package/dist/test/DevLoginStaleCookieFirstHit.test.js.map +1 -1
  33. package/dist/test/DevMail.test.d.ts +2 -0
  34. package/dist/test/DevMail.test.d.ts.map +1 -0
  35. package/dist/test/DevMail.test.js +250 -0
  36. package/dist/test/DevMail.test.js.map +1 -0
  37. package/dist/test/ExecutePasswordReset.test.js +513 -124
  38. package/dist/test/ExecutePasswordReset.test.js.map +1 -1
  39. package/dist/test/InitiatePasswordReset.test.d.ts +2 -0
  40. package/dist/test/InitiatePasswordReset.test.d.ts.map +1 -0
  41. package/dist/test/InitiatePasswordReset.test.js +415 -0
  42. package/dist/test/InitiatePasswordReset.test.js.map +1 -0
  43. package/dist/test/LogCapture.d.ts +22 -0
  44. package/dist/test/LogCapture.d.ts.map +1 -0
  45. package/dist/test/LogCapture.js +127 -0
  46. package/dist/test/LogCapture.js.map +1 -0
  47. package/dist/test/MalformedResetTokens.d.ts +11 -0
  48. package/dist/test/MalformedResetTokens.d.ts.map +1 -0
  49. package/dist/test/MalformedResetTokens.js +35 -0
  50. package/dist/test/MalformedResetTokens.js.map +1 -0
  51. package/dist/test/ValidateResetToken.test.js +156 -33
  52. package/dist/test/ValidateResetToken.test.js.map +1 -1
  53. package/dist/test/emulatorLock.test.d.ts +2 -0
  54. package/dist/test/emulatorLock.test.d.ts.map +1 -0
  55. package/dist/test/emulatorLock.test.js +32 -0
  56. package/dist/test/emulatorLock.test.js.map +1 -0
  57. package/dist/test/emulatorLockName.test.d.ts +2 -0
  58. package/dist/test/emulatorLockName.test.d.ts.map +1 -0
  59. package/dist/test/emulatorLockName.test.js +36 -0
  60. package/dist/test/emulatorLockName.test.js.map +1 -0
  61. package/generated/index.ts +5 -1
  62. package/index.ts +1 -1
  63. package/jest.config.js +2 -2
  64. package/package.json +4 -4
  65. package/src/authentication/PasswordResetToken.ts +74 -20
  66. package/src/routes/devLogin.ts +1 -1
  67. package/src/routes/devMail.ts +72 -0
  68. package/src/routes/executePasswordReset.ts +3 -2
  69. package/src/routes/initiatePasswordReset.ts +20 -17
  70. package/test/DbTraffic.ts +43 -0
  71. package/test/DevLoginBootstrapAdmin.test.ts +3 -4
  72. package/test/DevLoginStaleCookieFirstHit.test.ts +1 -1
  73. package/test/DevMail.test.ts +151 -0
  74. package/test/ExecutePasswordReset.test.ts +308 -70
  75. package/test/InitiatePasswordReset.test.ts +250 -0
  76. package/test/LogCapture.ts +54 -0
  77. package/test/MalformedResetTokens.ts +27 -0
  78. package/test/ValidateResetToken.test.ts +108 -24
  79. package/test/emulatorLock.js +5 -6
  80. package/test/emulatorLock.test.ts +32 -0
  81. package/test/emulatorLockName.test.ts +33 -0
@@ -1,3 +1,4 @@
1
+ import { Moment } from 'moment';
1
2
  import { User } from '@proteinjs/user';
2
3
  export type PasswordResetResolution = {
3
4
  status: 'malformed';
@@ -15,30 +16,60 @@ export type PasswordResetResolution = {
15
16
  * The password-reset token's one owner: its shape, its mint, the lookup that maps a presented
16
17
  * value back to the user it was issued to, and its single-use redemption.
17
18
  *
18
- * A token is 32 bytes from the platform CSPRNG, hex-encoded (64 lowercase hex characters),
19
- * stored on the user row beside its expiry until it is redeemed or expires.
19
+ * A token is 32 bytes from the platform CSPRNG, hex-encoded (64 lowercase hex characters). The
20
+ * token itself exists only in the link mailed to the account: the user row stores its SHA-256
21
+ * digest (hex) beside the expiry, never the token, so a read of the table yields nothing that
22
+ * can be presented. A digest is not a token — presenting one is hashed again and matches no row.
23
+ * A fast unsalted hash is enough here because the input is 256 bits of CSPRNG output, not a
24
+ * human-chosen secret.
20
25
  *
21
- * Resolution refuses anything that is not a value of that shape BEFORE any lookup. A query
22
- * filter built from a request value that is not a token does not compare the way a token does:
23
- * `null` renders as `IS NULL` and would match every account with no pending reset, an empty
24
- * string matches an emptied column, and other types reach the driver. The row the lookup
25
- * returns is then re-checked in code — its stored token must be a string equal to the presented
26
- * one (compared in constant time) and its expiry a real timestamp still in the future — so the
26
+ * Resolution refuses anything that is not a value of the token's shape BEFORE any lookup. A
27
+ * query filter built from a request value that is not a token does not compare the way a token
28
+ * does: `null` renders as `IS NULL` and would match every account with no pending reset, an
29
+ * empty string matches an emptied column, and other types reach the driver. The presented token
30
+ * is then hashed, the row is looked up by that digest, and the row the lookup returns is
31
+ * re-checked in code — its stored digest must be a string equal to the presented token's digest
32
+ * (compared in constant time) and its expiry a real timestamp still in the future — so the
27
33
  * outcome never rests on how the storage compares.
34
+ *
35
+ * No migration accompanies the move from a stored token to a stored digest: a row that still
36
+ * holds a token minted before it never equals the digest of anything presented, so that token
37
+ * fails validation for what remains of its hour and the person asks for a new link.
28
38
  */
29
39
  export declare class PasswordResetToken {
30
40
  private static readonly SHAPE;
31
- mint(): string;
41
+ private static readonly LIFETIME_MINUTES;
42
+ /**
43
+ * Issue a token to `user`: store its digest and its expiry on the row, replacing any token
44
+ * outstanding, and return the token — the only time it exists outside the mailed link.
45
+ */
46
+ mint(user: User): Promise<string>;
32
47
  resolve(presented: unknown): Promise<PasswordResetResolution>;
33
48
  /**
34
49
  * Write the new credential and clear the token in one conditional update: the row must still
35
- * carry this token at write time, so two presentations of the same token cannot both succeed.
36
- * Resolves false when the token was already redeemed.
50
+ * carry this token's digest at write time, so two presentations of the same token cannot both
51
+ * succeed. Resolves false when the token was already redeemed.
37
52
  */
38
53
  redeem(user: User, token: string, hashedPassword: string): Promise<boolean>;
39
- /** A log-safe reference to a presented token: a short digest of a well-formed one, never the value itself. */
54
+ /**
55
+ * Withdraw a token that never reached its owner (the mail carrying it failed to send): the row
56
+ * is cleared only while it still carries this token's digest, so a newer token is left alone.
57
+ */
58
+ revoke(user: User, token: string): Promise<void>;
59
+ /** When the token outstanding on `user` was minted, or undefined when the row carries none. */
60
+ mintedAt(user: User): Moment | undefined;
61
+ /**
62
+ * A log-safe reference to a presented token: a short prefix of a well-formed one's digest,
63
+ * never the value itself and never a part of it. The routes log it only for a token that
64
+ * resolved `unknown` — one no row carries — so it never lines up with a digest a row holds.
65
+ * What it is for is telling refusals apart in the log: one stale link presented again reads
66
+ * the same each time, a run of different values reads differently. 48 bits of the SHA-256 of
67
+ * 256 random bits identify nothing and can be presented nowhere.
68
+ */
40
69
  fingerprint(presented: unknown): string | undefined;
41
70
  private parse;
71
+ /** What the row stores in place of a token: its SHA-256, hex-encoded. */
72
+ private digest;
42
73
  private matches;
43
74
  private isLive;
44
75
  }
@@ -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
@@ -0,0 +1,4 @@
1
+ import { Route } from '@proteinjs/server-api';
2
+ export declare const devMail: Route;
3
+ export declare const devMailMessage: Route;
4
+ //# sourceMappingURL=devMail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devMail.d.ts","sourceRoot":"","sources":["../../../src/routes/devMail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAuB9C,eAAO,MAAM,OAAO,EAAE,KAwBrB,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,KAsB5B,CAAC"}
@@ -0,0 +1,119 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.devMailMessage = exports.devMail = void 0;
40
+ var email_server_1 = require("@proteinjs/email-server");
41
+ /**
42
+ * DEV-ONLY mail door: reads the process's mail sink (`@proteinjs/email-server` MailSink — the
43
+ * messages a development sender recorded instead of transporting), so automated dev-loop testing
44
+ * reads an invite or reset link from the sink instead of needing a real inbox, and nothing a dev
45
+ * server composes ever has to reach a real address to be verified.
46
+ *
47
+ * GET /dev/mail[?n=<count>] the last n messages (default 20, at most MailSink.CAPACITY), newest
48
+ * first: id, at, from, to, subject, the body's first link, refused, url
49
+ * GET /dev/mail/<id> the rendered message — its html part, or its text part as text/plain
50
+ *
51
+ * Double-gated exactly like `/dev/login` (devLogin.ts), acting only when BOTH hold; otherwise the
52
+ * paths answer 404 as if unregistered:
53
+ * 1. `process.env.DEVELOPMENT` — the dev-server switch, never set in prod images.
54
+ * 2. `DEV_AUTO_LOGIN_EMAIL` — the explicit per-launch dev opt-in.
55
+ */
56
+ var DEFAULT_COUNT = 20;
57
+ var gatesOpen = function () { var _a; return !!process.env.DEVELOPMENT && ((_a = process.env.DEV_AUTO_LOGIN_EMAIL) !== null && _a !== void 0 ? _a : '').trim().length > 0; };
58
+ exports.devMail = {
59
+ path: '/dev/mail',
60
+ method: 'get',
61
+ onRequest: function (request, response) { return __awaiter(void 0, void 0, void 0, function () {
62
+ var requested, count, messages;
63
+ var _a;
64
+ return __generator(this, function (_b) {
65
+ if (!gatesOpen()) {
66
+ response.status(404).send();
67
+ return [2 /*return*/];
68
+ }
69
+ requested = Number((_a = request.query) === null || _a === void 0 ? void 0 : _a.n);
70
+ count = Number.isInteger(requested) && requested > 0 ? Math.min(requested, email_server_1.MailSink.CAPACITY) : DEFAULT_COUNT;
71
+ messages = email_server_1.MailSink.get()
72
+ .list(count)
73
+ .map(function (_a) {
74
+ var id = _a.id, at = _a.at, from = _a.from, to = _a.to, subject = _a.subject, link = _a.link, refused = _a.refused;
75
+ return ({
76
+ id: id,
77
+ at: at,
78
+ from: from,
79
+ to: to,
80
+ subject: subject,
81
+ link: link,
82
+ refused: refused,
83
+ url: "/dev/mail/".concat(id),
84
+ });
85
+ });
86
+ response.status(200).json({ messages: messages });
87
+ return [2 /*return*/];
88
+ });
89
+ }); },
90
+ };
91
+ exports.devMailMessage = {
92
+ path: '/dev/mail/:id',
93
+ method: 'get',
94
+ onRequest: function (request, response) { return __awaiter(void 0, void 0, void 0, function () {
95
+ var record;
96
+ var _a, _b, _c;
97
+ return __generator(this, function (_d) {
98
+ if (!gatesOpen()) {
99
+ response.status(404).send();
100
+ return [2 /*return*/];
101
+ }
102
+ record = email_server_1.MailSink.get().get(String((_b = (_a = request.params) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : ''));
103
+ if (!record) {
104
+ response.status(404).send();
105
+ return [2 /*return*/];
106
+ }
107
+ if (record.html !== undefined) {
108
+ response.status(200).type('html').send(record.html);
109
+ return [2 /*return*/];
110
+ }
111
+ response
112
+ .status(200)
113
+ .type('text')
114
+ .send((_c = record.text) !== null && _c !== void 0 ? _c : '');
115
+ return [2 /*return*/];
116
+ });
117
+ }); },
118
+ };
119
+ //# sourceMappingURL=devMail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devMail.js","sourceRoot":"","sources":["../../../src/routes/devMail.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,wDAAmD;AAEnD;;;;;;;;;;;;;;GAcG;AACH,IAAM,aAAa,GAAG,EAAE,CAAC;AAEzB,IAAM,SAAS,GAAG,sBAChB,OAAA,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,MAAA,OAAO,CAAC,GAAG,CAAC,oBAAoB,mCAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAA,EAAA,CAAC;AAE7E,QAAA,OAAO,GAAU;IAC5B,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,KAAK;IACb,SAAS,EAAE,UAAO,OAAO,EAAE,QAAQ;;;;YACjC,IAAI,CAAC,SAAS,EAAE,EAAE;gBAChB,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;gBAC5B,sBAAO;aACR;YACK,SAAS,GAAG,MAAM,CAAC,MAAA,OAAO,CAAC,KAAK,0CAAE,CAAC,CAAC,CAAC;YACrC,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,uBAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;YAC9G,QAAQ,GAAG,uBAAQ,CAAC,GAAG,EAAE;iBAC5B,IAAI,CAAC,KAAK,CAAC;iBACX,GAAG,CAAC,UAAC,EAA4C;oBAA1C,EAAE,QAAA,EAAE,EAAE,QAAA,EAAE,IAAI,UAAA,EAAE,EAAE,QAAA,EAAE,OAAO,aAAA,EAAE,IAAI,UAAA,EAAE,OAAO,aAAA;gBAAO,OAAA,CAAC;oBACtD,EAAE,IAAA;oBACF,EAAE,IAAA;oBACF,IAAI,MAAA;oBACJ,EAAE,IAAA;oBACF,OAAO,SAAA;oBACP,IAAI,MAAA;oBACJ,OAAO,SAAA;oBACP,GAAG,EAAE,oBAAa,EAAE,CAAE;iBACvB,CAAC;YATqD,CASrD,CAAC,CAAC;YACN,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,UAAA,EAAE,CAAC,CAAC;;;SACzC;CACF,CAAC;AAEW,QAAA,cAAc,GAAU;IACnC,IAAI,EAAE,eAAe;IACrB,MAAM,EAAE,KAAK;IACb,SAAS,EAAE,UAAO,OAAO,EAAE,QAAQ;;;;YACjC,IAAI,CAAC,SAAS,EAAE,EAAE;gBAChB,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;gBAC5B,sBAAO;aACR;YACK,MAAM,GAAG,uBAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,MAAA,MAAA,OAAO,CAAC,MAAM,0CAAE,EAAE,mCAAI,EAAE,CAAC,CAAC,CAAC;YACpE,IAAI,CAAC,MAAM,EAAE;gBACX,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;gBAC5B,sBAAO;aACR;YACD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE;gBAC7B,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;gBACpD,sBAAO;aACR;YACD,QAAQ;iBACL,MAAM,CAAC,GAAG,CAAC;iBACX,IAAI,CAAC,MAAM,CAAC;iBACZ,IAAI,CAAC,MAAA,MAAM,CAAC,IAAI,mCAAI,EAAE,CAAC,CAAC;;;SAC5B;CACF,CAAC"}
@@ -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"}