@nxgt/janus 0.19.0 → 0.21.0

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 (52) hide show
  1. package/README.md +164 -10
  2. package/dist/auth/email-flows/reset-password.d.ts.map +1 -1
  3. package/dist/auth/events.d.ts +3 -3
  4. package/dist/auth/index.d.ts +1 -1
  5. package/dist/auth/index.d.ts.map +1 -1
  6. package/dist/auth/magic-link.d.ts +3 -0
  7. package/dist/auth/magic-link.d.ts.map +1 -1
  8. package/dist/auth/one-time/issue.d.ts +6 -0
  9. package/dist/auth/one-time/issue.d.ts.map +1 -1
  10. package/dist/auth/prepared.d.ts +21 -0
  11. package/dist/auth/prepared.d.ts.map +1 -0
  12. package/dist/auth/proven-sign-in.d.ts +22 -0
  13. package/dist/auth/proven-sign-in.d.ts.map +1 -0
  14. package/dist/auth/sign-in-code.d.ts +4 -0
  15. package/dist/auth/sign-in-code.d.ts.map +1 -1
  16. package/dist/auth/types/email-flows.d.ts +18 -0
  17. package/dist/auth/types/email-flows.d.ts.map +1 -1
  18. package/dist/auth/types/find-by-email.d.ts +31 -0
  19. package/dist/auth/types/find-by-email.d.ts.map +1 -0
  20. package/dist/auth/types/index.d.ts +3 -0
  21. package/dist/auth/types/index.d.ts.map +1 -1
  22. package/dist/auth/types/janus.d.ts +8 -5
  23. package/dist/auth/types/janus.d.ts.map +1 -1
  24. package/dist/auth/types/magic-link.d.ts +11 -0
  25. package/dist/auth/types/magic-link.d.ts.map +1 -1
  26. package/dist/auth/types/prepared.d.ts +41 -0
  27. package/dist/auth/types/prepared.d.ts.map +1 -0
  28. package/dist/auth/types/proven-sign-in.d.ts +43 -0
  29. package/dist/auth/types/proven-sign-in.d.ts.map +1 -0
  30. package/dist/auth/types/sign-in.d.ts +2 -2
  31. package/dist/auth/users/any-type-api.d.ts +2 -2
  32. package/dist/auth/users/any-type-api.d.ts.map +1 -1
  33. package/dist/auth/users/find-by-email.d.ts +11 -0
  34. package/dist/auth/users/find-by-email.d.ts.map +1 -0
  35. package/dist/auth/users/type-api.d.ts +2 -2
  36. package/dist/auth/users/type-api.d.ts.map +1 -1
  37. package/dist/index.js +105 -17
  38. package/dist/index.js.map +12 -9
  39. package/docs/README.md +2 -2
  40. package/docs/guide/devices.md +4 -0
  41. package/docs/guide/email-flows.md +105 -0
  42. package/docs/guide/errors.md +2 -2
  43. package/docs/guide/events.md +2 -2
  44. package/docs/guide/magic-link.md +49 -2
  45. package/docs/guide/passwords.md +5 -2
  46. package/docs/guide/second-factor.md +1 -1
  47. package/docs/guide/sessions.md +105 -1
  48. package/docs/guide/sign-in-code.md +54 -2
  49. package/docs/guide/users.md +70 -0
  50. package/docs/roadmap.md +24 -33
  51. package/docs/troubleshooting.md +100 -6
  52. package/package.json +1 -1
@@ -0,0 +1,41 @@
1
+ /**
2
+ * What `prepare` answers — `magicLink.prepare`, `signInCode.prepare`,
3
+ * `resetPassword.prepare`: a request for an e-mail, counted, not yet looked
4
+ * up.
5
+ *
6
+ * Part of what `janus()` hands back; `./index` gathers it.
7
+ */
8
+ import type { IssuedCode } from './email-flows';
9
+ /**
10
+ * A request counted by the mail throttle and not yet looked up: `send()`
11
+ * does the rest of what `request` does — looks the address up, issues, and
12
+ * spends the earlier tokens — and answers what `request` answers, `null`
13
+ * for an address nobody (active) holds.
14
+ *
15
+ * **`send()` runs once.** A second call is a `TypeError`, and so is a call
16
+ * after a first one failed: `prepare` again, which counts again.
17
+ */
18
+ export interface PreparedRequest<Issued> {
19
+ /**
20
+ * Looks the address up and issues, without counting again — the count
21
+ * was `prepare`'s. Send it off the visitor's request: its time tells
22
+ * whether the address has an account.
23
+ */
24
+ send(): Promise<Issued | null>;
25
+ }
26
+ /**
27
+ * A sign-in code's prepared request: the `challenge` is minted before
28
+ * anybody is looked up, so it goes to the visitor now, whoever holds the
29
+ * address — and replaces the decoy an unknown address needs. For an address
30
+ * nobody holds, `send()` answers `null` and the challenge is
31
+ * `TOKEN_UNKNOWN`; otherwise it is the `challenge` of what `send()` issues.
32
+ */
33
+ export interface PreparedCode<U> extends PreparedRequest<IssuedCode<U>> {
34
+ /**
35
+ * The secret the code will be checked against. **Keep it with the
36
+ * visitor** — a short-lived cookie, or the code form's body — never in
37
+ * the e-mail, a URL or a log.
38
+ */
39
+ readonly challenge: string;
40
+ }
41
+ //# sourceMappingURL=prepared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepared.d.ts","sourceRoot":"","sources":["../../../src/auth/types/prepared.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe,CAAC,MAAM;IACtC;;;;OAIG;IACH,IAAI,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,CAAE,SAAQ,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACtE;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * What every user type answers besides: signing in a user the application
3
+ * proved by its own means, with no credential janus checks.
4
+ *
5
+ * Part of what `janus()` hands back; `./index` gathers it.
6
+ */
7
+ import type { SignedIn, SignInOptions } from './sign-in';
8
+ import type { UserRef } from './user';
9
+ /**
10
+ * Signing in a user **the application has already proved** — by an
11
+ * invitation token it issued and the person followed, or a channel of its
12
+ * own it verified — without a password, a code or a link.
13
+ *
14
+ * `Answer` is `SignInResult` when the type may have a second factor: the
15
+ * application's proof is a first factor, and an active factor is still asked
16
+ * for.
17
+ */
18
+ export interface ProvenSignInApi<U, Answer = SignedIn<U>> {
19
+ /**
20
+ * Signs in the user of this type with this id, and answers what every
21
+ * other sign-in answers: a session, or a second-factor challenge when
22
+ * their factor is active — switch on `status`.
23
+ *
24
+ * **Privileged: janus checks no credential here, the application carries
25
+ * the proof.** Call it only after your server verified one — an
26
+ * invitation token it issued, a channel of its own — and never with an
27
+ * id a request supplied: whoever can choose the id signs in as anybody.
28
+ *
29
+ * Sends no e-mail, issues no token, counts nothing in the mail or the
30
+ * sign-in throttle, and **spends nothing**: links and codes already sent
31
+ * still work. Proves no e-mail either: `emailVerified` stays as it was.
32
+ *
33
+ * An inactive user is `USER_INACTIVE`, as for every sign-in. A malformed
34
+ * id, an unknown one or one of another type is `NOT_FOUND`, as for `get`:
35
+ * a sign-in that found nobody is never an answer to read past.
36
+ *
37
+ * Given a `device`, answers `newDevice` and `deviceToken`, and sends
38
+ * `user.newDeviceSignedIn` for a device new to the user. A challenge
39
+ * carries no device: give it again to `secondFactor.confirm`.
40
+ */
41
+ signInProven(user: UserRef, options?: SignInOptions): Promise<Answer>;
42
+ }
43
+ //# sourceMappingURL=proven-sign-in.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proven-sign-in.d.ts","sourceRoot":"","sources":["../../../src/auth/types/proven-sign-in.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC;IACvD;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACtE"}
@@ -33,8 +33,8 @@ export interface SignedIn<U> {
33
33
  }
34
34
  /**
35
35
  * What a sign-in takes besides its credentials: `signUp`, `signIn`,
36
- * `secondFactor.confirm`, `secondFactor.recover`, `signInCode.confirm` and
37
- * `magicLink.confirm`.
36
+ * `secondFactor.confirm`, `secondFactor.recover`, `signInCode.confirm`,
37
+ * `magicLink.confirm` and `signInProven`.
38
38
  */
39
39
  export interface SignInOptions {
40
40
  /**
@@ -1,6 +1,6 @@
1
1
  import type { AnyUser } from '../context';
2
- import type { MagicLinkApi, PasswordApi, ResetPasswordApi, SecondFactorApi, SignInCodeApi, SignInResult, StepUpApi, StepUpByApp, StepUpByEmail, UserTypeApi, VerifyEmailApi } from '../types';
2
+ import type { FindByEmailApi, MagicLinkApi, PasswordApi, ProvenSignInApi, ResetPasswordApi, SecondFactorApi, SignInCodeApi, SignInResult, StepUpApi, StepUpByApp, StepUpByEmail, UserTypeApi, VerifyEmailApi } from '../types';
3
3
  import type { Input } from './flow-types';
4
4
  /** Everything one user type answers. Which flows it has is decided by its types; all are built. */
5
- export type AnyTypeApi = UserTypeApi<AnyUser, Input> & PasswordApi<AnyUser, Input, string, SignInResult<AnyUser>> & SecondFactorApi<AnyUser> & SignInCodeApi<AnyUser, SignInResult<AnyUser>> & MagicLinkApi<AnyUser, SignInResult<AnyUser>> & StepUpApi<AnyUser, StepUpByEmail<AnyUser> | StepUpByApp<AnyUser>> & VerifyEmailApi<AnyUser> & ResetPasswordApi<AnyUser>;
5
+ export type AnyTypeApi = UserTypeApi<AnyUser, Input> & PasswordApi<AnyUser, Input, string, SignInResult<AnyUser>> & SecondFactorApi<AnyUser> & ProvenSignInApi<AnyUser, SignInResult<AnyUser>> & SignInCodeApi<AnyUser, SignInResult<AnyUser>> & MagicLinkApi<AnyUser, SignInResult<AnyUser>> & StepUpApi<AnyUser, StepUpByEmail<AnyUser> | StepUpByApp<AnyUser>> & VerifyEmailApi<AnyUser> & FindByEmailApi<AnyUser> & ResetPasswordApi<AnyUser>;
6
6
  //# sourceMappingURL=any-type-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"any-type-api.d.ts","sourceRoot":"","sources":["../../../src/auth/users/any-type-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,KAAK,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,YAAY,EACZ,SAAS,EACT,WAAW,EACX,aAAa,EACb,WAAW,EACX,cAAc,EACd,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,mGAAmG;AACnG,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,GACnD,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,GAC1D,eAAe,CAAC,OAAO,CAAC,GACxB,aAAa,CAAC,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,GAC7C,YAAY,CAAC,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,GAC5C,SAAS,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC,GACjE,cAAc,CAAC,OAAO,CAAC,GACvB,gBAAgB,CAAC,OAAO,CAAC,CAAC"}
1
+ {"version":3,"file":"any-type-api.d.ts","sourceRoot":"","sources":["../../../src/auth/users/any-type-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,KAAK,EACX,cAAc,EACd,YAAY,EACZ,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,YAAY,EACZ,SAAS,EACT,WAAW,EACX,aAAa,EACb,WAAW,EACX,cAAc,EACd,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,mGAAmG;AACnG,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,GACnD,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,GAC1D,eAAe,CAAC,OAAO,CAAC,GACxB,eAAe,CAAC,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,GAC/C,aAAa,CAAC,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,GAC7C,YAAY,CAAC,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,GAC5C,SAAS,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC,GACjE,cAAc,CAAC,OAAO,CAAC,GACvB,cAAc,CAAC,OAAO,CAAC,GACvB,gBAAgB,CAAC,OAAO,CAAC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { ResolvedType } from '../config';
2
+ import { type AnyUser, type Context } from '../context';
3
+ import type { FindByEmailApi } from '../types';
4
+ /**
5
+ * `findByEmail` for one user type: the holder the e-mail flows would find —
6
+ * the same normalisation, the same comparison with the stored address — with
7
+ * nothing counted, issued or written. A value that is no address is
8
+ * nobody's, as a malformed id is for `find`, and the store is not asked.
9
+ */
10
+ export declare function findByEmailFlows(context: Context, type: ResolvedType): FindByEmailApi<AnyUser>;
11
+ //# sourceMappingURL=find-by-email.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-by-email.d.ts","sourceRoot":"","sources":["../../../src/auth/users/find-by-email.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,OAAO,EAAyB,MAAM,YAAY,CAAC;AAC/E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC/B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,GAChB,cAAc,CAAC,OAAO,CAAC,CASzB"}
@@ -3,8 +3,8 @@ import type { Context } from '../context';
3
3
  import type { AnyTypeApi } from './any-type-api';
4
4
  /**
5
5
  * Everything one user type answers, gathered from its flows: its records,
6
- * its password, its second factor, its sign-in code and link, its step-up
7
- * and its e-mails.
6
+ * its password, its sign-in on the application's proof, its second factor,
7
+ * its sign-in code and link, its step-up, its e-mails and the lookup by e-mail.
8
8
  */
9
9
  export declare function typeApi(context: Context, type: ResolvedType): AnyTypeApi;
10
10
  //# sourceMappingURL=type-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"type-api.d.ts","sourceRoot":"","sources":["../../../src/auth/users/type-api.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAM1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAIjD;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,GAAG,UAAU,CAiBxE"}
1
+ {"version":3,"file":"type-api.d.ts","sourceRoot":"","sources":["../../../src/auth/users/type-api.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAO1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAKjD;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,GAAG,UAAU,CAmBxE"}
package/dist/index.js CHANGED
@@ -1153,7 +1153,7 @@ async function issueCode(context, token) {
1153
1153
  }
1154
1154
  async function insertOneTime(context, token, code) {
1155
1155
  const now = context.clock.now();
1156
- const secret = mintSecret();
1156
+ const secret = token.secret ?? mintSecret();
1157
1157
  const expiresAt = new Date(now.getTime() + token.ttlMs);
1158
1158
  await context.store.tokens.insertToken({
1159
1159
  tokenHash: hashSecret(secret),
@@ -1295,6 +1295,18 @@ async function heldByPassword(context, type, verified, password, result, where)
1295
1295
  return false;
1296
1296
  }
1297
1297
 
1298
+ // src/auth/prepared.ts
1299
+ function sendOnce(where, issue) {
1300
+ let sent = false;
1301
+ return async () => {
1302
+ if (sent) {
1303
+ throw new TypeError(`${where}(…).send: already called — a prepared request sends once; call ${where} again for another`);
1304
+ }
1305
+ sent = true;
1306
+ return issue();
1307
+ };
1308
+ }
1309
+
1298
1310
  // src/auth/email-flows/email-token.ts
1299
1311
  async function redeemEmailToken(context, type, kind, secret, where) {
1300
1312
  const token = await spendOneTime(context, secret, kind, where, "token");
@@ -1327,20 +1339,32 @@ function resetPasswordFlows(context, type, at) {
1327
1339
  return {
1328
1340
  async request(email) {
1329
1341
  const where = at("resetPassword.request");
1330
- passwordRule(type, where);
1331
- await countMailByAddress(context, type, "resetPassword", String(email), where);
1332
- const record = await holderOfEmail(context, type, String(email));
1333
- if (record === null)
1334
- return null;
1335
- const issued = await issueEmailToken(context, type, "resetPassword", record, where);
1336
- await context.store.tokens.spendUserTokens(record.id, "resetPassword", context.clock.now(), hashSecret(issued.token));
1337
- return { ...issued, user: toUser(record) };
1342
+ return (await prepareReset(context, type, String(email), where)).send();
1343
+ },
1344
+ async prepare(email) {
1345
+ const where = at("resetPassword.prepare");
1346
+ return prepareReset(context, type, String(email), where);
1338
1347
  },
1339
1348
  async confirm(secret, password) {
1340
1349
  return confirmReset(context, type, secret, password, at("resetPassword.confirm"));
1341
1350
  }
1342
1351
  };
1343
1352
  }
1353
+ async function prepareReset(context, type, email, where) {
1354
+ passwordRule(type, where);
1355
+ await countMailByAddress(context, type, "resetPassword", email, where);
1356
+ return Object.freeze({
1357
+ send: sendOnce(where, () => issueReset(context, type, email, where))
1358
+ });
1359
+ }
1360
+ async function issueReset(context, type, email, where) {
1361
+ const record = await holderOfEmail(context, type, email);
1362
+ if (record === null)
1363
+ return null;
1364
+ const issued = await issueEmailToken(context, type, "resetPassword", record, where);
1365
+ await context.store.tokens.spendUserTokens(record.id, "resetPassword", context.clock.now(), hashSecret(issued.token));
1366
+ return { ...issued, user: toUser(record) };
1367
+ }
1344
1368
  async function confirmReset(context, type, secret, password, where) {
1345
1369
  passwordRule(type, where);
1346
1370
  checkPassword(type, password, where);
@@ -1561,7 +1585,12 @@ async function finishEmailSignIn(context, type, finish, signIn) {
1561
1585
  function magicLinkFlows(context, type, at, finish) {
1562
1586
  return {
1563
1587
  async request(email) {
1564
- return requestLink(context, type, String(email), at("magicLink.request"));
1588
+ const where = at("magicLink.request");
1589
+ return (await prepareLink(context, type, String(email), where)).send();
1590
+ },
1591
+ async prepare(email) {
1592
+ const where = at("magicLink.prepare");
1593
+ return prepareLink(context, type, String(email), where);
1565
1594
  },
1566
1595
  async confirm(token, options) {
1567
1596
  const where = at("magicLink.confirm");
@@ -1570,8 +1599,13 @@ function magicLinkFlows(context, type, at, finish) {
1570
1599
  }
1571
1600
  };
1572
1601
  }
1573
- async function requestLink(context, type, email, where) {
1602
+ async function prepareLink(context, type, email, where) {
1574
1603
  await countMailByAddress(context, type, "magicLink", email, where);
1604
+ return Object.freeze({
1605
+ send: sendOnce(where, () => issueLink(context, type, email))
1606
+ });
1607
+ }
1608
+ async function issueLink(context, type, email) {
1575
1609
  const record = await signInHolder(context, type, email);
1576
1610
  if (record === null)
1577
1611
  return null;
@@ -1596,6 +1630,32 @@ async function confirmLink(context, type, finish, secret, where, device) {
1596
1630
  });
1597
1631
  }
1598
1632
 
1633
+ // src/auth/proven-sign-in.ts
1634
+ function provenSignInFlows(context, type, at, finish) {
1635
+ return {
1636
+ async signInProven(user, options) {
1637
+ const where = at("signInProven");
1638
+ const device = deviceHint(context, options, where);
1639
+ const record = await getRecord(context, idOf2(user), type.name, where);
1640
+ if (!record.active) {
1641
+ throw new UserInactiveError2(`${where}: the user is inactive`, {
1642
+ userId: record.id,
1643
+ userType: type.name
1644
+ });
1645
+ }
1646
+ const result = await finish(record, where, device);
1647
+ await reportNewDevice(context, result);
1648
+ return result;
1649
+ }
1650
+ };
1651
+ }
1652
+ function idOf2(user) {
1653
+ if (typeof user === "string")
1654
+ return user;
1655
+ const id = typeof user === "object" && user !== null ? user.id : undefined;
1656
+ return typeof id === "string" ? id : "";
1657
+ }
1658
+
1599
1659
  // src/auth/sign-in-attempts.ts
1600
1660
  var PURPOSE3 = "janus/sign-in/attempts/v1";
1601
1661
  async function countLogin(context, type, login, where) {
@@ -1974,29 +2034,43 @@ function secondFactorFlows(context, type, at) {
1974
2034
  function signInCodeFlows(context, type, at, finish) {
1975
2035
  return {
1976
2036
  async request(email) {
1977
- return requestCode(context, type, String(email), at("signInCode.request"));
2037
+ const where = at("signInCode.request");
2038
+ return (await prepareCode(context, type, String(email), where)).send();
2039
+ },
2040
+ async prepare(email) {
2041
+ const where = at("signInCode.prepare");
2042
+ return prepareCode(context, type, String(email), where);
1978
2043
  },
1979
2044
  async confirm(challenge, code, options) {
1980
2045
  return confirmCode(context, type, finish, { challenge, code, options }, at);
1981
2046
  }
1982
2047
  };
1983
2048
  }
1984
- async function requestCode(context, type, email, where) {
2049
+ async function prepareCode(context, type, email, where) {
1985
2050
  await countMailByAddress(context, type, "signInCode", email, where);
2051
+ const challenge = mintSecret();
2052
+ return Object.freeze({
2053
+ challenge,
2054
+ send: sendOnce(where, () => issueSignInCode(context, type, email, challenge))
2055
+ });
2056
+ }
2057
+ async function issueSignInCode(context, type, email, challenge) {
1986
2058
  const record = await signInHolder(context, type, email);
1987
2059
  if (record === null)
1988
2060
  return null;
2061
+ const address = String(record.fields[type.email]);
1989
2062
  const { secret, code, expiresAt } = await issueCode(context, {
1990
2063
  kind: "signInCode",
1991
2064
  userId: record.id,
1992
- address: String(record.fields[type.email]),
1993
- ttlMs: context.config.tokenTtlMs.signInCode
2065
+ address,
2066
+ ttlMs: context.config.tokenTtlMs.signInCode,
2067
+ secret: challenge
1994
2068
  });
1995
2069
  await keepOnlyLatest(context, record.id, "signInCode", secret);
1996
2070
  return {
1997
2071
  code,
1998
2072
  challenge: secret,
1999
- email: String(record.fields[type.email]),
2073
+ email: address,
2000
2074
  expiresAt,
2001
2075
  user: toUser(record)
2002
2076
  };
@@ -2164,6 +2238,18 @@ async function confirmStepUp(context, type, request, secret, code, where) {
2164
2238
  return toSession(confirmed);
2165
2239
  }
2166
2240
 
2241
+ // src/auth/users/find-by-email.ts
2242
+ function findByEmailFlows(context, type) {
2243
+ return {
2244
+ async findByEmail(email) {
2245
+ if (typeof email !== "string" || email.trim() === "")
2246
+ return null;
2247
+ const record = await holderOfEmail(context, type, email);
2248
+ return record === null ? null : toUser(record);
2249
+ }
2250
+ };
2251
+ }
2252
+
2167
2253
  // src/auth/users/by-login.ts
2168
2254
  var byLogin = async (context, type, login) => isStorable(login) ? context.store.users.findUserByLogin(type.name, login) : null;
2169
2255
 
@@ -2448,10 +2534,12 @@ function typeApi(context, type) {
2448
2534
  return {
2449
2535
  ...recordFlows(context, type, at),
2450
2536
  ...passwordFlows(context, type, at, secondFactor.finish),
2537
+ ...provenSignInFlows(context, type, at, secondFactor.finish),
2451
2538
  secondFactor: secondFactor.api,
2452
2539
  signInCode,
2453
2540
  magicLink: magicLinkFlows(context, type, at, secondFactor.finish),
2454
2541
  stepUp: stepUpFlows(context, type, at),
2542
+ ...findByEmailFlows(context, type),
2455
2543
  ...emailFlows(context, type, at)
2456
2544
  };
2457
2545
  }
@@ -2540,5 +2628,5 @@ export {
2540
2628
  systemClock
2541
2629
  };
2542
2630
 
2543
- //# debugId=22ACDB89AAF4B56564756E2164756E21
2631
+ //# debugId=8864B5F285AA35A664756E2164756E21
2544
2632
  //# sourceMappingURL=index.js.map