@fonderie/auth 7.5.0 → 7.6.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/dist/index.d.cts CHANGED
@@ -2,8 +2,8 @@ export { IMfaChallenge, ISession, IUser } from './types.cjs';
2
2
  import { IFonderieModule, IReadinessProblem, IFonderieApp } from '@fonderie/core';
3
3
  import { IStoreAdapter } from '@fonderie/store';
4
4
  import { EventBus } from '@fonderie/events';
5
- import { I as IAuthConfig } from './session-Dwlcgxww.cjs';
6
- export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, f as IDataExportContributor, M as MESSAGE_KEYS, g as buildAuthAccountLimiter, h as buildAuthIpLimiter, w as withSession } from './session-Dwlcgxww.cjs';
5
+ import { I as IAuthConfig } from './session-Bv4qu9xV.cjs';
6
+ export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, f as IDataExportContributor, M as MESSAGE_KEYS, g as buildAuthAccountLimiter, h as buildAuthIpLimiter, w as withSession } from './session-Bv4qu9xV.cjs';
7
7
  export { IUserDTO, toUserDTO } from './dtos/user.cjs';
8
8
  export { requireAuth, validate } from '@fonderie/core/middlewares';
9
9
  import { z } from 'zod';
@@ -63,6 +63,11 @@ declare const DEFAULT_TEMPLATES: {
63
63
  html: string;
64
64
  text: string;
65
65
  };
66
+ "password-revoked": {
67
+ subject: string;
68
+ html: string;
69
+ text: string;
70
+ };
66
71
  "oauth-registration": {
67
72
  subject: string;
68
73
  html: string;
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ export { IMfaChallenge, ISession, IUser } from './types.js';
2
2
  import { IFonderieModule, IReadinessProblem, IFonderieApp } from '@fonderie/core';
3
3
  import { IStoreAdapter } from '@fonderie/store';
4
4
  import { EventBus } from '@fonderie/events';
5
- import { I as IAuthConfig } from './session-Dwlcgxww.js';
6
- export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, f as IDataExportContributor, M as MESSAGE_KEYS, g as buildAuthAccountLimiter, h as buildAuthIpLimiter, w as withSession } from './session-Dwlcgxww.js';
5
+ import { I as IAuthConfig } from './session-Bv4qu9xV.js';
6
+ export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, f as IDataExportContributor, M as MESSAGE_KEYS, g as buildAuthAccountLimiter, h as buildAuthIpLimiter, w as withSession } from './session-Bv4qu9xV.js';
7
7
  export { IUserDTO, toUserDTO } from './dtos/user.js';
8
8
  export { requireAuth, validate } from '@fonderie/core/middlewares';
9
9
  import { z } from 'zod';
@@ -63,6 +63,11 @@ declare const DEFAULT_TEMPLATES: {
63
63
  html: string;
64
64
  text: string;
65
65
  };
66
+ "password-revoked": {
67
+ subject: string;
68
+ html: string;
69
+ text: string;
70
+ };
66
71
  "oauth-registration": {
67
72
  subject: string;
68
73
  html: string;
package/dist/index.js CHANGED
@@ -260,6 +260,11 @@ var MESSAGE_KEYS = {
260
260
  // Changing HOW an account can be signed into is a security event for the
261
261
  // account's owner, who may not be the person doing it. Both directions are
262
262
  // notified for the same reason password changes are.
263
+ // A password was REVOKED because the account it sat on had never been
264
+ // verified and an OAuth identity proved ownership of the mailbox. The
265
+ // recipient is now known to own that address, so this reaches the right
266
+ // person — and it is the only way they learn a password stopped working.
267
+ passwordRevoked: "password-revoked",
263
268
  // Welcome for an account CREATED by an OAuth sign-in. Distinct from
264
269
  // emailRegistration, which carries a verification PIN — an OAuth signup
265
270
  // arrives already verified by the provider and has nothing to confirm.
@@ -750,22 +755,49 @@ var UserModel = class {
750
755
  * The prior state is read in a CTE of the SAME statement, so it sees the
751
756
  * snapshot from before the insert. Reading it in a separate query would
752
757
  * race two concurrent logins into both seeing "new".
758
+ *
759
+ * SECURITY — linking is BY EMAIL, so this merges into whatever row already
760
+ * holds the address. If that row was never verified, its password was never
761
+ * proven to belong to the mailbox; anyone can register an address they do
762
+ * not own. The provider HAS proven ownership. So an unverified password is
763
+ * revoked at the moment of linking: without that, linking sets
764
+ * email_verified_at and the earlier registrant is left holding a working
765
+ * password on an account now treated as verified.
766
+ *
767
+ * The legitimate user loses nothing but a password reset — they own the
768
+ * mailbox. Someone who does not own it cannot receive that mail.
753
769
  */
754
770
  async upsertByProvider(email2, provider, providerId) {
755
771
  const [row] = await this.store.query(
756
772
  `WITH prior AS (
757
- SELECT provider FROM fonderie_users WHERE email = $1
773
+ SELECT provider, email_verified_at, password_hash IS NOT NULL AS had_password
774
+ FROM fonderie_users WHERE email = $1
758
775
  ), upserted AS (
759
776
  INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)
760
777
  VALUES ($1, now(), $2, $3)
761
778
  ON CONFLICT (email) DO UPDATE
762
779
  SET provider = $2, provider_id = $3,
763
- email_verified_at = COALESCE(fonderie_users.email_verified_at, now())
780
+ email_verified_at = COALESCE(fonderie_users.email_verified_at, now()),
781
+ -- A password on an UNVERIFIED account is a claim, not a
782
+ -- credential: nobody ever proved that mailbox belongs to
783
+ -- whoever set it. The provider has now proven it belongs to
784
+ -- the person signing in. Drop the unproven claim rather than
785
+ -- promote it \u2014 otherwise linking marks the account verified
786
+ -- and silently hands the earlier registrant a working
787
+ -- password on a now-trusted account.
788
+ password_hash = CASE
789
+ WHEN fonderie_users.email_verified_at IS NULL THEN NULL
790
+ ELSE fonderie_users.password_hash
791
+ END
764
792
  RETURNING id
765
793
  )
766
794
  SELECT upserted.id,
767
795
  NOT EXISTS (SELECT 1 FROM prior) AS inserted,
768
- (SELECT provider FROM prior) AS "previousProvider"
796
+ (SELECT provider FROM prior) AS "previousProvider",
797
+ -- True only when a password was actually revoked, so the
798
+ -- caller can tell the owner their sign-in method changed.
799
+ COALESCE((SELECT had_password AND email_verified_at IS NULL FROM prior), false)
800
+ AS "clearedUnverifiedPassword"
769
801
  FROM upserted`,
770
802
  [email2, provider, providerId]
771
803
  );
@@ -2475,6 +2507,18 @@ function oauthController(store, config, bus) {
2475
2507
  const reqOpts = reqId !== void 0 ? { requestId: reqId } : void 0;
2476
2508
  const label = provider.charAt(0).toUpperCase() + provider.slice(1);
2477
2509
  const recipient = { email: user.email, phone: null, deviceToken: null };
2510
+ if (upserted.clearedUnverifiedPassword && user.email) {
2511
+ await background5(bus.emit(
2512
+ NOTIFICATION_EVENT4,
2513
+ {
2514
+ type: MESSAGE_KEYS.passwordRevoked,
2515
+ locale: user.locale,
2516
+ data: { provider: label },
2517
+ recipient
2518
+ },
2519
+ reqOpts
2520
+ ));
2521
+ }
2478
2522
  if (upserted.inserted) {
2479
2523
  await background5(bus.emit(
2480
2524
  EVENT_KEYS.userRegistered,
@@ -3231,6 +3275,24 @@ If you made this change, you're all set. If not, contact support right away \u20
3231
3275
  The phone number on your account was just updated.
3232
3276
 
3233
3277
  If you made this change, you're all set. If not, contact support right away \u2014 someone may have access to your account.`
3278
+ },
3279
+ [MESSAGE_KEYS.passwordRevoked]: {
3280
+ subject: "The password on your account was removed",
3281
+ html: `<h1>Your password was removed</h1>
3282
+ <p>You just signed in with <strong>{{provider}}</strong>. A password had been set on this account, but this email address had never been confirmed &mdash; so we could not tell who set it, and it has been removed.</p>
3283
+ <p>Signing in with {{provider}} works as before. If you want a password as well, set one from your account settings.</p>
3284
+ <p class="muted">If you never set a password here, someone else may have tried to register with your address. Nothing further is needed &mdash; the account is yours.</p>`,
3285
+ text: `Your password was removed
3286
+
3287
+ You just signed in with {{provider}}. A password had been set on this account,
3288
+ but this email address had never been confirmed \u2014 so we could not tell who set
3289
+ it, and it has been removed.
3290
+
3291
+ Signing in with {{provider}} works as before. If you want a password as well,
3292
+ set one from your account settings.
3293
+
3294
+ If you never set a password here, someone else may have tried to register with
3295
+ your address. Nothing further is needed \u2014 the account is yours.`
3234
3296
  },
3235
3297
  [MESSAGE_KEYS.oauthRegistration]: {
3236
3298
  subject: "Welcome to {{appName}}",
@@ -3269,6 +3331,7 @@ If you did this, you're all set. If not, contact support right away.`
3269
3331
  }
3270
3332
  };
3271
3333
  var SAMPLE_PAYLOADS = {
3334
+ [MESSAGE_KEYS.passwordRevoked]: { provider: "Google" },
3272
3335
  [MESSAGE_KEYS.oauthRegistration]: { provider: "Google", appName: "Fonderie" },
3273
3336
  [MESSAGE_KEYS.oauthLinked]: { provider: "Google" },
3274
3337
  [MESSAGE_KEYS.oauthUnlinked]: { provider: "Google" },