@ultimabase/password 0.1.2 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @ultimabase/password
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f3a1fac: `User.checkPassword({ id, password }, { as, client })` checks the current password before a sensitive action, opens no session, and is counted per user even without a client. A wrong current password, here and in `changePassword`, now answers `Wrong password.`
8
+ - fcca799: `User.closeAccount({ id }, { as })` closes an account and keeps its row: it ends the user's sessions, unlinks their accounts, and runs the listeners sign-in methods register with `AuthApi.onCloseAccount`. password's deletes the hash, the tokens and the email requests, and sets `emailVerified` to `false`, so the email signs up again.
9
+ - 99cdfec: `password()` adds a unique `email` field to the user model, which the user and Master read and only Master writes; the password account follows it. `requestEmailChange` and `confirmEmailChange` change the email from a link sent to the new address, and `sendEmail` tells the old one with an `'emailChanged'` email. `AuthApi.unlinkAccount` unlinks one method's account.
10
+
11
+ Breaking: `Email` is a union, so read `token` after checking `kind`; a user model that declares its own `email` stops startup; users stored before this release read `email: null` until a Master `User.update({ where: { id }, data: { email } })` writes their account's email.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [fcca799]
16
+ - Updated dependencies [99cdfec]
17
+ - Updated dependencies [2c3bf4e]
18
+ - Updated dependencies [fd1263e]
19
+ - Updated dependencies [6d4f980]
20
+ - @ultimabase/auth@0.2.0
21
+ - ultimabase@0.2.0
22
+
3
23
  ## 0.1.2
4
24
 
5
25
  ### Patch Changes
package/dist/core.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { AuthApi } from '@ultimabase/auth';
2
2
  import type { Client, ExposedModel, Logger, Row } from 'ultimabase';
3
+ import type { SendEmail } from './email-functions.ts';
3
4
  import type { Attempt } from './models.ts';
4
5
  /** The method `AuthAccount` links a password's email with (D-113). */
5
6
  export declare const method = "password";
@@ -12,6 +13,7 @@ export interface State {
12
13
  readonly logger: Logger;
13
14
  /** The sends under way, which stopping waits for (D-117). */
14
15
  readonly sending: Set<Promise<void>>;
16
+ readonly sendEmail: SendEmail | undefined;
15
17
  }
16
18
  /** What a sign-in returns: its session, and the user read as itself. */
17
19
  export interface SignedIn<U = Row> {
@@ -28,7 +30,8 @@ export interface SignedIn<U = Row> {
28
30
  export declare const attemptOf: (subject: string, client: Client | undefined) => Attempt;
29
31
  /**
30
32
  * Creates the user, its account and its hash in one transaction, as Master:
31
- * the caller's fields were checked against the list (D-113).
33
+ * the caller's fields were checked against the list (D-113). The email's
34
+ * hook links the account (D-162).
32
35
  */
33
36
  export declare function createUser(state: State, email: string, password: string, data: object): Promise<{
34
37
  readonly user: Row;
@@ -44,8 +47,9 @@ export declare function verified(state: State, email: string, password: string,
44
47
  /** Checks `userId`'s current password, counting the attempt. */
45
48
  export declare function checkCurrent(userId: string, password: string, attempt: Attempt): Promise<void>;
46
49
  /**
47
- * Replaces `userId`'s hash, ends every session of the user and deletes its
48
- * pending reset tokens (D-117); the new stamp.
50
+ * Replaces `userId`'s hash, or gives one to an email without (D-162), ends
51
+ * every session of the user and deletes its pending reset tokens (D-117);
52
+ * the new stamp.
49
53
  */
50
54
  export declare function setPassword(state: State, userId: string, password: string): Promise<string>;
51
55
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,KAAK,EACX,MAAM,EAEN,YAAY,EACZ,MAAM,EAEN,GAAG,EAEH,MAAM,YAAY,CAAA;AAInB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAK1C,sEAAsE;AACtE,eAAO,MAAM,MAAM,aAAa,CAAA;AAEhC,qEAAqE;AACrE,MAAM,WAAW,KAAK;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAA;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,KAAK,CAAA;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;CACpC;AAED,wEAAwE;AACxE,MAAM,WAAW,QAAQ,CAAC,CAAC,GAAG,GAAG;IAChC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAA;CACvB;AAKD,4EAA4E;AAC5E,eAAO,MAAM,SAAS,YACZ,MAAM,UACP,MAAM,GAAG,SAAS,KACxB,OACqE,CAAA;AAExE;;;GAGG;AACH,wBAAsB,UAAU,CAC/B,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CASzD;AAyCD,uEAAuE;AACvE,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACtB;AAED,wEAAwE;AACxE,wBAAsB,QAAQ,CAC7B,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,GACd,OAAO,CAAC,QAAQ,CAAC,CAOnB;AAED,gEAAgE;AAChE,wBAAsB,YAAY,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,GACd,OAAO,CAAC,IAAI,CAAC,CAIf;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAChC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAmBjB;AAaD;;;;GAIG;AACH,wBAAsB,QAAQ,CAC7B,KAAK,EAAE,KAAK,EACZ,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,QAAQ,EAC3B,MAAM,EAAE,MAAM,GAAG,SAAS,GACxB,OAAO,CAAC,QAAQ,CAAC,CAQnB"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,KAAK,EACX,MAAM,EAEN,YAAY,EACZ,MAAM,EAEN,GAAG,EAEH,MAAM,YAAY,CAAA;AAGnB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAErD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAK1C,sEAAsE;AACtE,eAAO,MAAM,MAAM,aAAa,CAAA;AAEhC,qEAAqE;AACrE,MAAM,WAAW,KAAK;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAA;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,KAAK,CAAA;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;IACpC,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,SAAS,CAAA;CACzC;AAED,wEAAwE;AACxE,MAAM,WAAW,QAAQ,CAAC,CAAC,GAAG,GAAG;IAChC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAA;CACvB;AASD,4EAA4E;AAC5E,eAAO,MAAM,SAAS,YACZ,MAAM,UACP,MAAM,GAAG,SAAS,KACxB,OACqE,CAAA;AAExE;;;;GAIG;AACH,wBAAsB,UAAU,CAC/B,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAWzD;AAyCD,uEAAuE;AACvE,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACtB;AAED,wEAAwE;AACxE,wBAAsB,QAAQ,CAC7B,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,GACd,OAAO,CAAC,QAAQ,CAAC,CAOnB;AAED,gEAAgE;AAChE,wBAAsB,YAAY,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,GACd,OAAO,CAAC,IAAI,CAAC,CAIf;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAChC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAyBjB;AAaD;;;;GAIG;AACH,wBAAsB,QAAQ,CAC7B,KAAK,EAAE,KAAK,EACZ,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,QAAQ,EAC3B,MAAM,EAAE,MAAM,GAAG,SAAS,GACxB,OAAO,CAAC,QAAQ,CAAC,CAQnB"}
package/dist/core.js CHANGED
@@ -10,18 +10,20 @@ const M = { as: master };
10
10
  /** The method `AuthAccount` links a password's email with (D-113). */
11
11
  export const method = 'password';
12
12
  const invalid = () => new PublicError('INVALID_CREDENTIALS', 'Wrong email or password.');
13
+ /** The current password is known to be the user's: no email to name (D-161). */
14
+ const wrongPassword = () => new PublicError('INVALID_CREDENTIALS', 'Wrong password.');
13
15
  /** The attempt a client makes for `subject`; server code is not counted. */
14
16
  export const attemptOf = (subject, client) => client === undefined ? undefined : { subject, address: client.address };
15
17
  /**
16
18
  * Creates the user, its account and its hash in one transaction, as Master:
17
- * the caller's fields were checked against the list (D-113).
19
+ * the caller's fields were checked against the list (D-113). The email's
20
+ * hook links the account (D-162).
18
21
  */
19
22
  export async function createUser(state, email, password, data) {
20
23
  const hash = await hashPassword(password);
21
24
  const stamp = randomUUID();
22
25
  return transaction(async () => {
23
- const user = await state.users.queries.create({ data }, M);
24
- await state.auth.linkAccount(user.id, method, email);
26
+ const user = await state.users.queries.create({ data: { ...data, email } }, M);
25
27
  await PasswordHash.create({ data: { userId: user.id, hash, stamp } }, M);
26
28
  return { user, stamp };
27
29
  });
@@ -73,22 +75,28 @@ export async function verified(state, email, password, attempt) {
73
75
  export async function checkCurrent(userId, password, attempt) {
74
76
  await limited(attempt, async () => {
75
77
  if ((await matches(userId, password)) === undefined)
76
- throw invalid();
78
+ throw wrongPassword();
77
79
  });
78
80
  }
79
81
  /**
80
- * Replaces `userId`'s hash, ends every session of the user and deletes its
81
- * pending reset tokens (D-117); the new stamp.
82
+ * Replaces `userId`'s hash, or gives one to an email without (D-162), ends
83
+ * every session of the user and deletes its pending reset tokens (D-117);
84
+ * the new stamp.
82
85
  */
83
86
  export async function setPassword(state, userId, password) {
84
87
  const stored = await PasswordHash.findUnique({ where: { userId } }, M);
85
- if (stored === null) {
88
+ const linked = stored !== null ||
89
+ (await state.auth.subjectOf(userId, method)) !== undefined;
90
+ if (!linked) {
86
91
  throw new PublicError('NOT_FOUND', `User ${userId} has no password: sign it up with signUp.`);
87
92
  }
88
93
  const hash = await hashPassword(password);
89
94
  const stamp = randomUUID();
90
95
  await transaction(async () => {
91
- await PasswordHash.update({ where: { userId }, data: { hash, stamp } }, M);
96
+ const data = { hash, stamp };
97
+ await (stored === null
98
+ ? PasswordHash.create({ data: { userId, ...data } }, M)
99
+ : PasswordHash.update({ where: { userId }, data }, M));
92
100
  await PasswordToken.deleteMany({ where: { userId, kind: 'reset' } }, M);
93
101
  await state.auth.revokeSessions(userId);
94
102
  });
@@ -0,0 +1,35 @@
1
+ import type { Caller } from 'ultimabase';
2
+ import type { State } from './core.ts';
3
+ type Emailed = {
4
+ readonly id: string;
5
+ readonly email: string | null;
6
+ };
7
+ type Draft = {
8
+ email?: string | null | undefined;
9
+ emailVerified?: boolean | undefined;
10
+ };
11
+ /** Only the user itself and Master read the email. */
12
+ export declare const readsEmail: (facts: {
13
+ readonly caller: Caller;
14
+ readonly record: {
15
+ readonly id: string;
16
+ };
17
+ }) => boolean;
18
+ /** Normalizes a created user's email, as at sign-up. */
19
+ export declare function creating(data: Draft): void;
20
+ /** Links the created user's email as its password account. */
21
+ export declare function created(state: State, record: Emailed): Promise<void>;
22
+ /**
23
+ * Normalizes an updated email, refuses `null` while the account is linked,
24
+ * and unverifies another email unless the write verifies it.
25
+ */
26
+ export declare function updating(state: State, record: Emailed, data: Draft): Promise<void>;
27
+ /**
28
+ * Moves the password account to the new email, in the write's transaction,
29
+ * and deletes the links the old one received.
30
+ */
31
+ export declare function updated(state: State, record: Emailed, previous: Emailed): Promise<void>;
32
+ /** Tells the old address, once the change commits, when `sendEmail` exists. */
33
+ export declare function committed(state: State, record: Emailed, previous: Emailed): void;
34
+ export {};
35
+ //# sourceMappingURL=email-field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-field.d.ts","sourceRoot":"","sources":["../src/email-field.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAExC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AAQtC,KAAK,OAAO,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AACrE,KAAK,KAAK,GAAG;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IACjC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CACnC,CAAA;AAED,sDAAsD;AACtD,eAAO,MAAM,UAAU,UAAW;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CACxC,KAAG,OAKH,CAAA;AAED,wDAAwD;AACxD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,IAAI,CAI1C;AAED,8DAA8D;AAC9D,wBAAsB,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1E;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAC7B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,KAAK,GACT,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAC5B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,OAAO,GACf,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,+EAA+E;AAC/E,wBAAgB,SAAS,CACxB,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,OAAO,GACf,IAAI,CAcN"}
@@ -0,0 +1,77 @@
1
+ import { isMaster, master, PublicError } from 'ultimabase';
2
+ import { method } from './core.js';
3
+ import { later } from './email-functions.js';
4
+ import { emailOf, valid } from './input.js';
5
+ import { PasswordToken } from './tokens.js';
6
+ const M = { as: master };
7
+ /** Only the user itself and Master read the email. */
8
+ export const readsEmail = (facts) => {
9
+ const { caller, record } = facts;
10
+ return (isMaster(caller) || (caller.kind === 'user' && caller.user.id === record.id));
11
+ };
12
+ /** Normalizes a created user's email, as at sign-up. */
13
+ export function creating(data) {
14
+ if (typeof data.email === 'string') {
15
+ data.email = valid(emailOf, data.email, 'email');
16
+ }
17
+ }
18
+ /** Links the created user's email as its password account. */
19
+ export async function created(state, record) {
20
+ if (record.email === null)
21
+ return;
22
+ await state.auth.linkAccount(record.id, method, record.email);
23
+ }
24
+ /**
25
+ * Normalizes an updated email, refuses `null` while the account is linked,
26
+ * and unverifies another email unless the write verifies it.
27
+ */
28
+ export async function updating(state, record, data) {
29
+ if (data.email === undefined)
30
+ return;
31
+ const subject = await state.auth.subjectOf(record.id, method);
32
+ if (data.email === null) {
33
+ if (subject === undefined)
34
+ return;
35
+ const message = 'closeAccount removes the email.';
36
+ throw new PublicError('VALIDATION_FAILED', `email: ${message}`, {
37
+ details: { issues: [{ path: ['email'], message }] },
38
+ });
39
+ }
40
+ data.email = valid(emailOf, data.email, 'email');
41
+ if (data.email !== subject)
42
+ data.emailVerified ??= false;
43
+ }
44
+ /**
45
+ * Moves the password account to the new email, in the write's transaction,
46
+ * and deletes the links the old one received.
47
+ */
48
+ export async function updated(state, record, previous) {
49
+ if (record.email === null || record.email === previous.email)
50
+ return;
51
+ const { auth } = state;
52
+ const subject = await auth.subjectOf(record.id, method);
53
+ // Filling the field from the account changes nothing else.
54
+ if (subject === record.email)
55
+ return;
56
+ if (subject !== undefined)
57
+ await auth.unlinkAccount(record.id, method);
58
+ await auth.linkAccount(record.id, method, record.email);
59
+ const where = { userId: record.id, kind: { in: ['reset', 'verify'] } };
60
+ await PasswordToken.deleteMany({ where }, M);
61
+ }
62
+ /** Tells the old address, once the change commits, when `sendEmail` exists. */
63
+ export function committed(state, record, previous) {
64
+ const { sendEmail } = state;
65
+ const [from, to] = [previous.email, record.email];
66
+ if (sendEmail === undefined || from === null || to === null || from === to) {
67
+ return;
68
+ }
69
+ later(state, async () => {
70
+ await sendEmail({
71
+ kind: 'emailChanged',
72
+ to: from,
73
+ newEmail: to,
74
+ userId: record.id,
75
+ });
76
+ });
77
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"email-functions.d.ts","sourceRoot":"","sources":["../src/email-functions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAiC,WAAW,EAAE,MAAM,YAAY,CAAA;AACvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AAStC,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAGnD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAUnE;AAED,6CAA6C;AAC7C,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAEvD;;;GAGG;AACH,MAAM,MAAM,MAAM,GAAG,CACpB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,KACN,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAAA;AAErC,eAAO,MAAM,QAAQ,cACR,SAAS,KAAG,MAOvB,CAAA;AAWF,4EAA4E;AAC5E,wBAAgB,cAAc,CAC7B,OAAO,EAAE,MAAM,KAAK,EACpB,IAAI,EAAE,MAAM,GACV,SAAS,aAAa,EAAE,CAmE1B"}
1
+ {"version":3,"file":"email-functions.d.ts","sourceRoot":"","sources":["../src/email-functions.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAIN,WAAW,EAEX,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AAWtC,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAGnD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAUnE;AAED,6CAA6C;AAC7C,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAEvD;;;GAGG;AACH,MAAM,MAAM,MAAM,GAAG,CACpB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,KACN,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAAA;AAErC,eAAO,MAAM,QAAQ,cACR,SAAS,KAAG,MAOvB,CAAA;AAWF,4EAA4E;AAC5E,wBAAgB,cAAc,CAC7B,OAAO,EAAE,MAAM,KAAK,EACpB,IAAI,EAAE,MAAM,GACV,SAAS,aAAa,EAAE,CAqE1B"}
@@ -1,6 +1,6 @@
1
- import { allow, defineFunction, master, PublicError } from 'ultimabase';
2
- import { method, setPassword } from './core.js';
3
- import { outputSchema, requestResetInput, resetPasswordInput, verifyEmailInput, } from './input.js';
1
+ import { allow, defineFunction, master, PublicError, transaction, } from 'ultimabase';
2
+ import { attemptOf, checkCurrent, method, setPassword } from './core.js';
3
+ import { confirmEmailChangeInput, outputSchema, requestEmailChangeInput, requestResetInput, resetPasswordInput, verifyEmailInput, } from './input.js';
4
4
  import { requestLimited } from './models.js';
5
5
  import { emailLimited, issueToken, redeemToken } from './tokens.js';
6
6
  /**
@@ -22,7 +22,7 @@ export const mailerOf = (sendEmail) => async (kind, userId, to) => {
22
22
  const refused = await emailLimited(userId);
23
23
  if (refused !== undefined)
24
24
  return refused;
25
- const token = await issueToken(userId, kind);
25
+ const token = await issueToken(userId, kind, to);
26
26
  await sendEmail({ kind, to, token, userId });
27
27
  return undefined;
28
28
  };
@@ -60,7 +60,7 @@ export function emailFunctions(stateOf, mail) {
60
60
  output: done,
61
61
  run: async ({ input: { input } }) => {
62
62
  const state = stateOf();
63
- const userId = await redeemToken(input.token, 'reset');
63
+ const { userId } = await redeemToken(input.token, 'reset');
64
64
  await setPassword(state, userId, input.password);
65
65
  await verify(state, userId);
66
66
  return true;
@@ -71,7 +71,8 @@ export function emailFunctions(stateOf, mail) {
71
71
  input: verifyEmailInput,
72
72
  output: done,
73
73
  run: async ({ input: { input } }) => {
74
- await verify(stateOf(), await redeemToken(input.token, 'verify'));
74
+ const { userId } = await redeemToken(input.token, 'verify');
75
+ await verify(stateOf(), userId);
75
76
  return true;
76
77
  },
77
78
  }).with({ access: allow.public });
@@ -103,5 +104,46 @@ export function emailFunctions(stateOf, mail) {
103
104
  resetPassword,
104
105
  verifyEmail,
105
106
  sendVerificationEmail,
107
+ ...emailChangeFunctions(stateOf, mail),
106
108
  ];
107
109
  }
110
+ /** `requestEmailChange` and `confirmEmailChange` (D-162). */
111
+ function emailChangeFunctions(stateOf, mail) {
112
+ const requestEmailChange = defineFunction({
113
+ name: 'requestEmailChange',
114
+ input: requestEmailChangeInput,
115
+ output: done,
116
+ run: async ({ input: { input }, caller, client }) => {
117
+ if (caller.kind !== 'user') {
118
+ throw new PublicError('UNAUTHENTICATED', 'requestEmailChange needs a signed-in user.');
119
+ }
120
+ const { id } = caller.user;
121
+ await checkCurrent(id, input.currentPassword, attemptOf(id, client));
122
+ const { newEmail } = input;
123
+ // The caller proved the password: telling it is no enumeration.
124
+ if ((await stateOf().auth.accountOf(method, newEmail)) !== undefined) {
125
+ throw new PublicError('UNIQUE_VIOLATION', 'This email has an account.');
126
+ }
127
+ const refused = await mail('emailChange', id, newEmail);
128
+ if (refused !== undefined)
129
+ throw refused;
130
+ return true;
131
+ },
132
+ }).with({ access: allow.public });
133
+ const confirmEmailChange = defineFunction({
134
+ name: 'confirmEmailChange',
135
+ input: confirmEmailChangeInput,
136
+ output: done,
137
+ run: async ({ input: { input } }) => {
138
+ const { users } = stateOf();
139
+ // A taken email undoes the redeem too: the link works until it expires.
140
+ await transaction(async () => {
141
+ const { userId, email } = await redeemToken(input.token, 'emailChange');
142
+ const data = { email, emailVerified: true };
143
+ await users.queries.update({ where: { id: userId }, data }, M);
144
+ });
145
+ return true;
146
+ },
147
+ }).with({ access: allow.public });
148
+ return [requestEmailChange, confirmEmailChange];
149
+ }
package/dist/input.d.ts CHANGED
@@ -96,6 +96,18 @@ export declare const verifyEmailInput: StandardSchema<Named<{
96
96
  }>, Named<{
97
97
  readonly token: string;
98
98
  }>>;
99
+ type RequestEmailChange = {
100
+ readonly newEmail: string;
101
+ readonly currentPassword: string;
102
+ };
103
+ /** What `requestEmailChange` takes: the new email and the password (D-162). */
104
+ export declare const requestEmailChangeInput: StandardSchema<Named<RequestEmailChange>, Named<RequestEmailChange>>;
105
+ /** What `confirmEmailChange` takes: the email's token (D-162). */
106
+ export declare const confirmEmailChangeInput: StandardSchema<Named<{
107
+ readonly token: string;
108
+ }>, Named<{
109
+ readonly token: string;
110
+ }>>;
99
111
  /** The email, the password and the listed user fields, by their names. */
100
112
  export declare const signUpInput: (fields: ReadonlyMap<string, Property>) => StandardSchema<Named<SignUp>, Named<SignUp>>;
101
113
  /** An output's schema: typed and exported, never validated (D-45). */
@@ -1 +1 @@
1
- {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"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"}
package/dist/input.js CHANGED
@@ -210,6 +210,13 @@ export const resetPasswordInput = namedInput('ResetPassword', new Map([
210
210
  ]));
211
211
  /** What `verifyEmail` takes: the email's token (D-117). */
212
212
  export const verifyEmailInput = namedInput('VerifyEmail', new Map([['token', property(text, string)]]));
213
+ /** What `requestEmailChange` takes: the new email and the password (D-162). */
214
+ export const requestEmailChangeInput = namedInput('RequestEmailChange', new Map([
215
+ ['newEmail', email],
216
+ ['currentPassword', property(text, string)],
217
+ ]));
218
+ /** What `confirmEmailChange` takes: the email's token (D-162). */
219
+ export const confirmEmailChangeInput = namedInput('ConfirmEmailChange', new Map([['token', property(text, string)]]));
213
220
  /** The email, the password and the listed user fields, by their names. */
214
221
  export const signUpInput = (fields) => namedInput('SignUp', new Map([
215
222
  ['email', email],
package/dist/models.d.ts CHANGED
@@ -31,16 +31,20 @@ export declare const PasswordRequest: ModelOf<'PasswordRequest', RequestFields,
31
31
  * (D-114).
32
32
  */
33
33
  export declare function networkOf(address: string): string;
34
- /** Where an attempt comes from; `undefined` for server code, not counted. */
34
+ /**
35
+ * Where an attempt comes from: a client's address, none for server code
36
+ * that is still counted (D-161), `undefined` for server code that is not.
37
+ */
35
38
  export type Attempt = {
36
39
  readonly subject: string;
37
- readonly address: string;
40
+ readonly address?: string;
38
41
  } | undefined;
39
42
  /**
40
43
  * Counts an attempt as failed until `run` succeeds, so concurrent attempts
41
44
  * cannot pass the limit (D-113, D-114). Within 15 minutes, the 11th from one
42
- * address on a subject is refused, as the 101st on it or from the address;
43
- * a success clears its address's count, a busy refusal is not counted.
45
+ * address on a subject is refused, as the 101st on it or from the address,
46
+ * and the 11th without an address on a subject (D-161); a success clears
47
+ * its address's count, a busy refusal is not counted.
44
48
  */
45
49
  export declare function limited<T>(attempt: Attempt, run: () => Promise<T>): Promise<T>;
46
50
  /** What the hourly limits count (D-114, D-117). */
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC/D,OAAO,EAAuB,WAAW,EAAE,MAAM,YAAY,CAAA;AAO7D,eAAO,MAAM,IAAI,EAAE,MAAuB,CAAA;AAE1C,KAAK,UAAU,GAAG;IACjB,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;KAAE,CAAC,CAAA;IACzD,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IACnD,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;KAAE,CAAC,CAAA;CACxD,CAAA;AAED,eAAO,MAAM,YAAY,EAAE,OAAO,CAAC,cAAc,EAAE,UAAU,EAAE,EAAE,CAQ9D,CAAA;AAEH,KAAK,aAAa,GAAG;IACpB,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,CAAA;CAC7C,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,iBAAiB,EAAE,aAAa,EAAE,EAAE,CASvE,CAAA;AAEH,KAAK,aAAa,GAAG;IACpB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IACnD,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,CAAA;CAC7C,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,iBAAiB,EAAE,aAAa,EAAE,EAAE,CASvE,CAAA;AAEH;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMjD;AAYD,6EAA6E;AAC7E,MAAM,MAAM,OAAO,GAClB;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAA;AAenE;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,CAAC,EAC9B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAiCZ;AAED,mDAAmD;AACnD,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAA;AAEjE;;;;GAIG;AACH,wBAAsB,MAAM,CAC3B,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACV,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAmBlC;AAED,4FAA4F;AAC5F,wBAAsB,cAAc,CACnC,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAGf"}
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC/D,OAAO,EAAuB,WAAW,EAAE,MAAM,YAAY,CAAA;AAO7D,eAAO,MAAM,IAAI,EAAE,MAAuB,CAAA;AAE1C,KAAK,UAAU,GAAG;IACjB,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;KAAE,CAAC,CAAA;IACzD,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IACnD,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;KAAE,CAAC,CAAA;CACxD,CAAA;AAED,eAAO,MAAM,YAAY,EAAE,OAAO,CAAC,cAAc,EAAE,UAAU,EAAE,EAAE,CAQ9D,CAAA;AAEH,KAAK,aAAa,GAAG;IACpB,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,CAAA;CAC7C,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,iBAAiB,EAAE,aAAa,EAAE,EAAE,CASvE,CAAA;AAEH,KAAK,aAAa,GAAG;IACpB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IACnD,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,CAAA;CAC7C,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,iBAAiB,EAAE,aAAa,EAAE,EAAE,CASvE,CAAA;AAEH;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMjD;AAYD;;;GAGG;AACH,MAAM,MAAM,OAAO,GAClB;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAA;AAoCpE;;;;;;GAMG;AACH,wBAAsB,OAAO,CAAC,CAAC,EAC9B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CA+BZ;AAED,mDAAmD;AACnD,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAA;AAEjE;;;;GAIG;AACH,wBAAsB,MAAM,CAC3B,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACV,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAmBlC;AAED,4FAA4F;AAC5F,wBAAsB,cAAc,CACnC,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAGf"}
package/dist/models.js CHANGED
@@ -53,32 +53,46 @@ const tooMany = (oldest, span, now) => new PublicError('TOO_MANY_ATTEMPTS', 'Too
53
53
  retryAfter: Math.max(Math.ceil((oldest.getTime() + span - now) / 1000), 1),
54
54
  },
55
55
  });
56
+ /** What an attempt without an address is stored with; no address is it. */
57
+ const server = 'server';
56
58
  /** The `n`th most recent failure but `id` matching `where`, if there is one. */
57
59
  async function nth(where, id, n) {
58
60
  const failures = await PasswordAttempt.findMany({ where: { ...where, id: { not: id } }, orderBy: { at: 'desc' }, take: n }, M);
59
61
  return failures[n - 1]?.at;
60
62
  }
63
+ /**
64
+ * The failures but `id` that refuse the attempt: from one address, the 10th
65
+ * on the subject or the 100th on it or from the address; without one, the
66
+ * 10th on the subject from anywhere, and no address is filled (D-161).
67
+ */
68
+ async function reached(subject, address, id) {
69
+ if (address === undefined)
70
+ return [await nth({ subject }, id, limit)];
71
+ return [
72
+ await nth({ subject, address }, id, limit),
73
+ await nth({ subject }, id, ceiling),
74
+ await nth({ address }, id, ceiling),
75
+ ];
76
+ }
61
77
  /**
62
78
  * Counts an attempt as failed until `run` succeeds, so concurrent attempts
63
79
  * cannot pass the limit (D-113, D-114). Within 15 minutes, the 11th from one
64
- * address on a subject is refused, as the 101st on it or from the address;
65
- * a success clears its address's count, a busy refusal is not counted.
80
+ * address on a subject is refused, as the 101st on it or from the address,
81
+ * and the 11th without an address on a subject (D-161); a success clears
82
+ * its address's count, a busy refusal is not counted.
66
83
  */
67
84
  export async function limited(attempt, run) {
68
85
  if (attempt === undefined)
69
86
  return run();
70
87
  const { subject } = attempt;
71
- const address = networkOf(attempt.address);
88
+ const network = attempt.address === undefined ? undefined : networkOf(attempt.address);
89
+ const address = network ?? server;
72
90
  const now = Date.now();
73
91
  const since = new Date(now - window);
74
92
  await PasswordAttempt.deleteMany({ where: { at: { lte: since } } }, M);
75
93
  const { id } = await PasswordAttempt.create({ data: { subject, address, at: new Date(now) } }, M);
76
94
  // Several may be reached: the latest says when to retry.
77
- const [oldest] = [
78
- await nth({ subject, address }, id, limit),
79
- await nth({ subject }, id, ceiling),
80
- await nth({ address }, id, ceiling),
81
- ]
95
+ const [oldest] = (await reached(subject, network, id))
82
96
  .filter((at) => at !== undefined)
83
97
  .toSorted((a, b) => b.getTime() - a.getTime());
84
98
  if (oldest !== undefined) {
package/dist/plugin.d.ts CHANGED
@@ -45,6 +45,15 @@ export interface PasswordCommands<F> {
45
45
  readonly id: string;
46
46
  readonly password: string;
47
47
  }, options?: Options): Promise<void>;
48
+ /**
49
+ * Checks the user's current password before a sensitive action, as
50
+ * Master or as the user; opens no session. Always counted: with a
51
+ * `client` as changePassword, otherwise 10 failures per user (D-161).
52
+ */
53
+ checkPassword(args: {
54
+ readonly id: string;
55
+ readonly password: string;
56
+ }, options?: SignInOptions): Promise<void>;
48
57
  }
49
58
  /** password's types (D-57). */
50
59
  export interface Password extends PluginTypes {
@@ -55,6 +64,11 @@ export interface Password extends PluginTypes {
55
64
  readonly emailVerified: Field<boolean, {
56
65
  readonly default: false;
57
66
  }>;
67
+ /** The password account's email (D-162). */
68
+ readonly email: Field<string | null, {
69
+ readonly optional: true;
70
+ readonly unique: true;
71
+ }>;
58
72
  };
59
73
  readonly commands: PasswordCommands<this['fields']>;
60
74
  }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}