@fonderie/auth 7.4.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-CvNqy0Kd.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-CvNqy0Kd.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,26 @@ 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
+ };
71
+ "oauth-registration": {
72
+ subject: string;
73
+ html: string;
74
+ text: string;
75
+ };
76
+ "oauth-linked": {
77
+ subject: string;
78
+ html: string;
79
+ text: string;
80
+ };
81
+ "oauth-unlinked": {
82
+ subject: string;
83
+ html: string;
84
+ text: string;
85
+ };
66
86
  };
67
87
 
68
88
  interface ILoginEventDTO {
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-CvNqy0Kd.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-CvNqy0Kd.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,26 @@ 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
+ };
71
+ "oauth-registration": {
72
+ subject: string;
73
+ html: string;
74
+ text: string;
75
+ };
76
+ "oauth-linked": {
77
+ subject: string;
78
+ html: string;
79
+ text: string;
80
+ };
81
+ "oauth-unlinked": {
82
+ subject: string;
83
+ html: string;
84
+ text: string;
85
+ };
66
86
  };
67
87
 
68
88
  interface ILoginEventDTO {
package/dist/index.js CHANGED
@@ -256,7 +256,21 @@ var MESSAGE_KEYS = {
256
256
  mfaDisabled: "mfa-disabled",
257
257
  mfaBackupCodesRegenerated: "mfa-backup-codes-regenerated",
258
258
  emailChanged: "email-changed",
259
- phoneChanged: "phone-changed"
259
+ phoneChanged: "phone-changed",
260
+ // Changing HOW an account can be signed into is a security event for the
261
+ // account's owner, who may not be the person doing it. Both directions are
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",
268
+ // Welcome for an account CREATED by an OAuth sign-in. Distinct from
269
+ // emailRegistration, which carries a verification PIN — an OAuth signup
270
+ // arrives already verified by the provider and has nothing to confirm.
271
+ oauthRegistration: "oauth-registration",
272
+ oauthLinked: "oauth-linked",
273
+ oauthUnlinked: "oauth-unlinked"
260
274
  };
261
275
  var EVENT_KEYS = {
262
276
  userRegistered: "fonderie.user.registered",
@@ -488,6 +502,8 @@ function toUserDTO(user, phoneVerified = false) {
488
502
  isEmailVerified: user.emailVerifiedAt !== null,
489
503
  isPhoneVerified: phoneVerified,
490
504
  mfaEnabled: booleanOrFalse(user.mfaEnabled),
505
+ provider: stringOrEmpty(user.provider),
506
+ hasPassword: user.passwordHash !== null && user.passwordHash !== void 0,
491
507
  suspended: booleanOrFalse(user.suspended),
492
508
  whitelist: booleanOrFalse(user.whitelist),
493
509
  ipWhitelist: Array.isArray(user.ipWhitelist) ? user.ipWhitelist : [],
@@ -515,6 +531,7 @@ var USER_COLUMNS = `
515
531
  ip_whitelist AS "ipWhitelist",
516
532
  mfa_enabled AS "mfaEnabled",
517
533
  email_verified_at AS "emailVerifiedAt",
534
+ provider,
518
535
  deleted_at AS "deletedAt",
519
536
  created_at AS "createdAt",
520
537
  updated_at AS "updatedAt"
@@ -592,6 +609,31 @@ var UserModel = class {
592
609
  );
593
610
  return row ?? null;
594
611
  }
612
+ /**
613
+ * Clear the OAuth provider, but ONLY when the account can still be signed
614
+ * into afterwards.
615
+ *
616
+ * An account created through Google has no password. Unlinking it without
617
+ * checking would not be "removing a sign-in method" — it would be locking
618
+ * the owner out permanently, which is account deletion wearing a friendlier
619
+ * label. The guard lives in the WHERE clause so the check and the write are
620
+ * one atomic statement: a password cannot be removed between them.
621
+ *
622
+ * Returns false when the account has no password (nothing was changed) so
623
+ * the caller can say why rather than reporting a silent success.
624
+ */
625
+ async clearProvider(id) {
626
+ const rows = await this.store.query(
627
+ `UPDATE fonderie_users
628
+ SET provider = NULL, provider_id = NULL
629
+ WHERE id = $1
630
+ AND password_hash IS NOT NULL
631
+ AND provider IS NOT NULL
632
+ RETURNING id`,
633
+ [id]
634
+ );
635
+ return rows.length > 0;
636
+ }
595
637
  async updatePassword(id, passwordHash) {
596
638
  await this.store.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [
597
639
  passwordHash,
@@ -692,14 +734,71 @@ var UserModel = class {
692
734
  );
693
735
  return row?.mfa_secret ?? null;
694
736
  }
737
+ /**
738
+ * Link an OAuth identity to the account owning `email`, creating it if there
739
+ * is none.
740
+ *
741
+ * Returns what HAPPENED, not just who it happened to. Three outcomes hide
742
+ * behind one upsert, and callers must tell them apart:
743
+ *
744
+ * • `inserted` — a brand-new account. Whoever provisions on signup (the
745
+ * personal workspace, a wallet, a welcome email) must run, exactly as
746
+ * it does for a password registration. This is the case that was
747
+ * previously invisible: the upsert returned an id and looked identical
748
+ * to a returning user signing in, so OAuth signups silently skipped
749
+ * every signup side effect.
750
+ * • `previousProvider` differs — an existing account gained, or switched,
751
+ * a way to sign in. A security event for the owner.
752
+ * • `previousProvider` is the same — an ordinary login. Nothing to do, and
753
+ * notifying here would email the user on every single sign-in.
754
+ *
755
+ * The prior state is read in a CTE of the SAME statement, so it sees the
756
+ * snapshot from before the insert. Reading it in a separate query would
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.
769
+ */
695
770
  async upsertByProvider(email2, provider, providerId) {
696
771
  const [row] = await this.store.query(
697
- `INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)
698
- VALUES ($1, now(), $2, $3)
699
- ON CONFLICT (email) DO UPDATE
700
- SET provider = $2, provider_id = $3,
701
- email_verified_at = COALESCE(fonderie_users.email_verified_at, now())
702
- RETURNING id`,
772
+ `WITH prior AS (
773
+ SELECT provider, email_verified_at, password_hash IS NOT NULL AS had_password
774
+ FROM fonderie_users WHERE email = $1
775
+ ), upserted AS (
776
+ INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)
777
+ VALUES ($1, now(), $2, $3)
778
+ ON CONFLICT (email) DO UPDATE
779
+ SET provider = $2, provider_id = $3,
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
792
+ RETURNING id
793
+ )
794
+ SELECT upserted.id,
795
+ NOT EXISTS (SELECT 1 FROM prior) AS inserted,
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"
801
+ FROM upserted`,
703
802
  [email2, provider, providerId]
704
803
  );
705
804
  return row ?? null;
@@ -2192,6 +2291,50 @@ function userController(store, config, bus) {
2192
2291
  await sessions.deleteByUser(ctx.user.id);
2193
2292
  return setApiResponse4(HTTP4.OK, "PASSWORD_CHANGED", "Password updated successfully.");
2194
2293
  },
2294
+ /**
2295
+ * Remove an OAuth provider from the account.
2296
+ *
2297
+ * Refuses when the account has no password, because unlinking would
2298
+ * then leave NO way to sign in — that is account deletion, not a
2299
+ * settings toggle, and a user clicking "disconnect Google" is not
2300
+ * asking for it. The check is inside the UPDATE's WHERE clause, so a
2301
+ * password cannot disappear between the check and the write.
2302
+ *
2303
+ * Notifies the account's email either way: changing how an account can
2304
+ * be signed into is a security event for its owner, who is not
2305
+ * necessarily the person doing it.
2306
+ */
2307
+ unlinkOauth: async (ctx) => {
2308
+ const provider = String(ctx.meta["params"]?.["provider"] ?? "");
2309
+ const current = await users.findById(ctx.user.id);
2310
+ if (!current?.provider) {
2311
+ return setApiResponse4(HTTP4.NOT_FOUND, "NOT_LINKED", "No sign-in provider is linked.");
2312
+ }
2313
+ if (provider && provider.toLowerCase() !== current.provider.toLowerCase()) {
2314
+ return setApiResponse4(
2315
+ HTTP4.NOT_FOUND,
2316
+ "NOT_LINKED",
2317
+ `This account is not linked to ${provider}.`
2318
+ );
2319
+ }
2320
+ const removed = await users.clearProvider(ctx.user.id);
2321
+ if (!removed) {
2322
+ return setApiResponse4(
2323
+ HTTP4.CONFLICT,
2324
+ "PASSWORD_REQUIRED",
2325
+ "Set a password before disconnecting this provider \u2014 it is currently the only way to sign in."
2326
+ );
2327
+ }
2328
+ if (ctx.user.email) {
2329
+ await background4(bus?.emit(NOTIFICATION_EVENT3, {
2330
+ type: MESSAGE_KEYS.oauthUnlinked,
2331
+ locale: ctx.user.locale,
2332
+ data: { provider: current.provider },
2333
+ recipient: { email: ctx.user.email, phone: null, deviceToken: null }
2334
+ }));
2335
+ }
2336
+ return setApiResponse4(HTTP4.OK, "OAUTH_UNLINKED", "Sign-in provider removed.");
2337
+ },
2195
2338
  deleteMe: async (ctx) => {
2196
2339
  const userId = ctx.user.id;
2197
2340
  await users.softDelete(userId);
@@ -2251,7 +2394,8 @@ function userController(store, config, bus) {
2251
2394
  // src/controllers/oauth.controller.ts
2252
2395
  import { randomBytes as randomBytes4, createPublicKey, createHash as createHash2 } from "crypto";
2253
2396
  import jwt2 from "jsonwebtoken";
2254
- import { setApiResponse as setApiResponse5, HTTP as HTTP5, constantTimeEqual as constantTimeEqual2 } from "@fonderie/core";
2397
+ import { setApiResponse as setApiResponse5, HTTP as HTTP5, constantTimeEqual as constantTimeEqual2, background as background5 } from "@fonderie/core";
2398
+ import { NOTIFICATION_EVENT as NOTIFICATION_EVENT4 } from "@fonderie/events";
2255
2399
 
2256
2400
  // src/models/consumed-token.model.ts
2257
2401
  var ConsumedTokenModel = class {
@@ -2352,11 +2496,68 @@ async function verifyAppleIdToken(idToken, opts) {
2352
2496
  }
2353
2497
  }
2354
2498
  var appleEmailVerified = (c) => c.email_verified === true || c.email_verified === "true";
2355
- function oauthController(store, config) {
2499
+ function oauthController(store, config, bus) {
2356
2500
  const users = new UserModel(store);
2357
2501
  const sessions = new SessionModel(store);
2358
2502
  const loginEvents = new LoginEventModel(store);
2359
2503
  const consumedTokens = new ConsumedTokenModel(store);
2504
+ const announceOAuthUpsert = async (ctx, upserted, provider, user) => {
2505
+ if (!bus) return;
2506
+ const reqId = ctx.meta["requestId"];
2507
+ const reqOpts = reqId !== void 0 ? { requestId: reqId } : void 0;
2508
+ const label = provider.charAt(0).toUpperCase() + provider.slice(1);
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
+ }
2522
+ if (upserted.inserted) {
2523
+ await background5(bus.emit(
2524
+ EVENT_KEYS.userRegistered,
2525
+ {
2526
+ userId: upserted.id,
2527
+ email: user.email,
2528
+ firstName: user.firstName,
2529
+ lastName: user.lastName,
2530
+ loginMethod: provider
2531
+ },
2532
+ reqOpts
2533
+ ));
2534
+ if (user.email) {
2535
+ await background5(bus.emit(
2536
+ NOTIFICATION_EVENT4,
2537
+ {
2538
+ type: MESSAGE_KEYS.oauthRegistration,
2539
+ locale: user.locale,
2540
+ data: { provider: label, appName: config.appName ?? "Fonderie" },
2541
+ recipient
2542
+ },
2543
+ reqOpts
2544
+ ));
2545
+ }
2546
+ return;
2547
+ }
2548
+ if (upserted.previousProvider !== provider && user.email) {
2549
+ await background5(bus.emit(
2550
+ NOTIFICATION_EVENT4,
2551
+ {
2552
+ type: MESSAGE_KEYS.oauthLinked,
2553
+ locale: user.locale,
2554
+ data: { provider: label },
2555
+ recipient
2556
+ },
2557
+ reqOpts
2558
+ ));
2559
+ }
2560
+ };
2360
2561
  const completeAppleLogin = async (ctx, claims) => {
2361
2562
  const meta = requestMeta(ctx);
2362
2563
  if (!claims.email) {
@@ -2385,6 +2586,7 @@ function oauthController(store, config) {
2385
2586
  if (!fullUser) {
2386
2587
  return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR", "Apple login failed");
2387
2588
  }
2589
+ await announceOAuthUpsert(ctx, upserted, "apple", fullUser);
2388
2590
  const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, { loginMethod: "apple" });
2389
2591
  await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
2390
2592
  await loginEvents.recordSafe({
@@ -2535,6 +2737,7 @@ function oauthController(store, config) {
2535
2737
  if (!fullUser) {
2536
2738
  return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
2537
2739
  }
2740
+ await announceOAuthUpsert(ctx, upserted, "google", fullUser);
2538
2741
  const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, {
2539
2742
  loginMethod: "google"
2540
2743
  });
@@ -2699,7 +2902,7 @@ function oauthController(store, config) {
2699
2902
  function buildAuthRoutes(store, config, bus) {
2700
2903
  const user = userController(store, config, bus);
2701
2904
  const auth = authController(store, config, bus);
2702
- const oauth = oauthController(store, config);
2905
+ const oauth = oauthController(store, config, bus);
2703
2906
  const mfa = mfaController(store, config, config.appName ?? "Fonderie", bus);
2704
2907
  const verifyGate = config.requireVerification ? requireVerified : (_ctx, next) => next();
2705
2908
  const passthrough = (_ctx, next) => next();
@@ -2757,6 +2960,9 @@ function buildAuthRoutes(store, config, bus) {
2757
2960
  R("updateEmail", "PUT", "/users/email", requireAuth, verifyGate, validate(updateEmailSchema), user.updateEmail),
2758
2961
  R("updatePhone", "PUT", "/users/phone", requireAuth, verifyGate, validate(updatePhoneSchema), user.updatePhone),
2759
2962
  R("changePassword", "PUT", "/users/password", requireAuth, validate(changePasswordSchema), user.changePassword),
2963
+ // Disconnect an OAuth provider. requireAuth only — the controller
2964
+ // refuses when it would leave the account with no way to sign in.
2965
+ R("unlinkOauth", "DELETE", "/auth/oauth/:provider", requireAuth, user.unlinkOauth),
2760
2966
  R("deleteMe", "DELETE", "/users", requireAuth, verifyGate, user.deleteMe),
2761
2967
  R("exportMe", "GET", "/users/export", requireAuth, user.exportMe),
2762
2968
  // Security surfaces (Protected; the caller's own login history + sessions).
@@ -3069,9 +3275,66 @@ If you made this change, you're all set. If not, contact support right away \u20
3069
3275
  The phone number on your account was just updated.
3070
3276
 
3071
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.`
3296
+ },
3297
+ [MESSAGE_KEYS.oauthRegistration]: {
3298
+ subject: "Welcome to {{appName}}",
3299
+ html: `<h1>Welcome to {{appName}}</h1>
3300
+ <p>Your account was created using <strong>{{provider}}</strong>. Sign in any time with the same {{provider}} account &mdash; there is no password to remember.</p>
3301
+ <p class="muted">If you did not create this account, contact support.</p>`,
3302
+ text: `Welcome
3303
+
3304
+ Your account was created using {{provider}}. Sign in any time with the same
3305
+ {{provider}} account \u2014 there is no password to remember.
3306
+
3307
+ If you did not create this account, contact support.`
3308
+ },
3309
+ [MESSAGE_KEYS.oauthLinked]: {
3310
+ subject: "A new sign-in method was added to your account",
3311
+ html: `<h1>{{provider}} sign-in was added</h1>
3312
+ <p>Your account can now also be signed into with <strong>{{provider}}</strong>.</p>
3313
+ <p class="muted">If you did this, nothing more is needed. If not, contact support right away and change your password &mdash; someone else may be able to sign in as you.</p>`,
3314
+ text: `{{provider}} sign-in was added
3315
+
3316
+ Your account can now also be signed into with {{provider}}.
3317
+
3318
+ If you did this, nothing more is needed. If not, contact support right away and
3319
+ change your password \u2014 someone else may be able to sign in as you.`
3320
+ },
3321
+ [MESSAGE_KEYS.oauthUnlinked]: {
3322
+ subject: "A sign-in method was removed from your account",
3323
+ html: `<h1>{{provider}} sign-in was removed</h1>
3324
+ <p><strong>{{provider}}</strong> can no longer be used to sign in. Your email and password still work.</p>
3325
+ <p class="muted">If you did this, you&rsquo;re all set. If not, contact support right away.</p>`,
3326
+ text: `{{provider}} sign-in was removed
3327
+
3328
+ {{provider}} can no longer be used to sign in. Your email and password still work.
3329
+
3330
+ If you did this, you're all set. If not, contact support right away.`
3072
3331
  }
3073
3332
  };
3074
3333
  var SAMPLE_PAYLOADS = {
3334
+ [MESSAGE_KEYS.passwordRevoked]: { provider: "Google" },
3335
+ [MESSAGE_KEYS.oauthRegistration]: { provider: "Google", appName: "Fonderie" },
3336
+ [MESSAGE_KEYS.oauthLinked]: { provider: "Google" },
3337
+ [MESSAGE_KEYS.oauthUnlinked]: { provider: "Google" },
3075
3338
  [MESSAGE_KEYS.emailRegistration]: { firstName: "", pin: "123456" },
3076
3339
  [MESSAGE_KEYS.emailVerification]: { firstName: "Ada", pin: "123456" },
3077
3340
  [MESSAGE_KEYS.passwordReset]: {