@scalemule/nextjs 0.0.39 → 0.0.40

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.
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var money = require('@scalemule/money');
3
4
  var headers = require('next/headers');
4
5
  require('next/server');
5
6
 
@@ -689,6 +690,14 @@ var ScaleMuleServer = class {
689
690
  this.apiKey = config.apiKey;
690
691
  this.gatewayUrl = resolveGatewayUrl(config);
691
692
  this.debug = config.debug || false;
693
+ this.money = money.createMoneyClient({
694
+ apiKey: this.apiKey,
695
+ gatewayUrl: this.gatewayUrl,
696
+ fetch: globalThis.fetch.bind(globalThis)
697
+ });
698
+ }
699
+ moneyWithSession(sessionToken) {
700
+ return this.money.withAccessToken(sessionToken);
692
701
  }
693
702
  /**
694
703
  * Make a request to the ScaleMule API
@@ -767,6 +776,7 @@ function createServerClient(config) {
767
776
  }
768
777
  var SESSION_COOKIE_NAME = "sm_session";
769
778
  var USER_ID_COOKIE_NAME = "sm_user_id";
779
+ var KNOWN_ACCOUNTS_COOKIE_NAME = "sm_known_accounts";
770
780
  ({
771
781
  secure: process.env.NODE_ENV === "production"});
772
782
  function createCookieHeader(name, value, options = {}) {
@@ -847,6 +857,163 @@ async function getSession() {
847
857
  // Note: actual expiry is managed by ScaleMule backend
848
858
  };
849
859
  }
860
+ var MAX_KNOWN_ACCOUNTS = 10;
861
+ function maskEmail(email) {
862
+ const [local, domain] = email.split("@");
863
+ if (!domain) return "***@***.***";
864
+ const tldDot = domain.lastIndexOf(".");
865
+ const tld = tldDot > 0 ? domain.slice(tldDot) : "";
866
+ const domainBase = tldDot > 0 ? domain.slice(0, tldDot) : domain;
867
+ return `${local[0] || "*"}***@${domainBase[0] || "*"}***${tld}`;
868
+ }
869
+ function stableColorIndex(userId) {
870
+ let hash = 0;
871
+ for (let i = 0; i < userId.length; i++) {
872
+ hash = (hash << 5) - hash + userId.charCodeAt(i) | 0;
873
+ }
874
+ return Math.abs(hash) % 8;
875
+ }
876
+ function applyPrivacyToEntry(entry, privacy) {
877
+ switch (privacy) {
878
+ case "full":
879
+ return entry;
880
+ case "masked":
881
+ return {
882
+ userId: entry.userId,
883
+ email: entry.email ? maskEmail(entry.email) : void 0,
884
+ fullName: entry.fullName ? `${entry.fullName[0].toUpperCase()}.` : void 0,
885
+ provider: entry.provider,
886
+ lastActiveAt: entry.lastActiveAt,
887
+ colorIndex: stableColorIndex(entry.userId)
888
+ };
889
+ case "minimal":
890
+ return {
891
+ userId: entry.userId,
892
+ provider: entry.provider,
893
+ lastActiveAt: entry.lastActiveAt,
894
+ displayLabel: "Account",
895
+ colorIndex: stableColorIndex(entry.userId)
896
+ };
897
+ }
898
+ }
899
+ function appendKnownAccountCookie(headers, account, existingCookie, options = {}, privacy) {
900
+ let accounts = {};
901
+ if (existingCookie) {
902
+ try {
903
+ accounts = JSON.parse(decodeURIComponent(existingCookie));
904
+ } catch {
905
+ }
906
+ }
907
+ const effectivePrivacy = privacy || "full";
908
+ for (const [userId, entry] of Object.entries(accounts)) {
909
+ accounts[userId] = applyPrivacyToEntry(entry, effectivePrivacy);
910
+ }
911
+ accounts[account.userId] = applyPrivacyToEntry(account, effectivePrivacy);
912
+ const entries = Object.entries(accounts);
913
+ if (entries.length > MAX_KNOWN_ACCOUNTS) {
914
+ entries.sort((a, b) => new Date(b[1].lastActiveAt).getTime() - new Date(a[1].lastActiveAt).getTime());
915
+ accounts = Object.fromEntries(entries.slice(0, MAX_KNOWN_ACCOUNTS));
916
+ }
917
+ const maxAge = 365 * 24 * 60 * 60;
918
+ const secure = options.secure ?? process.env.NODE_ENV === "production";
919
+ const sameSite = options.sameSite ?? "lax";
920
+ const path = options.path ?? "/";
921
+ let cookie = `${KNOWN_ACCOUNTS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(accounts))}; Path=${path}; Max-Age=${maxAge}; SameSite=${sameSite}`;
922
+ if (secure) {
923
+ cookie += "; Secure";
924
+ }
925
+ if (options.domain) {
926
+ cookie += `; Domain=${options.domain}`;
927
+ }
928
+ headers.append("Set-Cookie", cookie);
929
+ }
930
+ function removeKnownAccountFromCookie(headers, userId, existingCookie, options = {}) {
931
+ let accounts = {};
932
+ if (existingCookie) {
933
+ try {
934
+ accounts = JSON.parse(decodeURIComponent(existingCookie));
935
+ } catch {
936
+ }
937
+ }
938
+ delete accounts[userId];
939
+ const maxAge = 365 * 24 * 60 * 60;
940
+ const secure = options.secure ?? process.env.NODE_ENV === "production";
941
+ const sameSite = options.sameSite ?? "lax";
942
+ const path = options.path ?? "/";
943
+ let cookie = `${KNOWN_ACCOUNTS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(accounts))}; Path=${path}; Max-Age=${maxAge}; SameSite=${sameSite}`;
944
+ if (secure) {
945
+ cookie += "; Secure";
946
+ }
947
+ if (options.domain) {
948
+ cookie += `; Domain=${options.domain}`;
949
+ }
950
+ headers.append("Set-Cookie", cookie);
951
+ }
952
+ function clearKnownAccountsCookie(headers, options = {}) {
953
+ const path = options.path ?? "/";
954
+ let cookie = `${KNOWN_ACCOUNTS_COOKIE_NAME}=; Path=${path}; Max-Age=0`;
955
+ if (options.domain) {
956
+ cookie += `; Domain=${options.domain}`;
957
+ }
958
+ headers.append("Set-Cookie", cookie);
959
+ }
960
+ function getKnownAccountsFromRequest(request) {
961
+ const cookieHeader = request.headers.get("cookie");
962
+ if (!cookieHeader) return [];
963
+ const cookies3 = Object.fromEntries(
964
+ cookieHeader.split(";").map((c) => {
965
+ const [key, ...rest] = c.trim().split("=");
966
+ return [key, decodeURIComponent(rest.join("="))];
967
+ })
968
+ );
969
+ const raw = cookies3[KNOWN_ACCOUNTS_COOKIE_NAME];
970
+ if (!raw) return [];
971
+ try {
972
+ const accounts = JSON.parse(raw);
973
+ return Object.values(accounts);
974
+ } catch {
975
+ return [];
976
+ }
977
+ }
978
+ function getKnownAccountsCookieRaw(request) {
979
+ const cookieHeader = request.headers.get("cookie");
980
+ if (!cookieHeader) return null;
981
+ const cookies3 = Object.fromEntries(
982
+ cookieHeader.split(";").map((c) => {
983
+ const [key, ...rest] = c.trim().split("=");
984
+ return [key, decodeURIComponent(rest.join("="))];
985
+ })
986
+ );
987
+ return cookies3[KNOWN_ACCOUNTS_COOKIE_NAME] || null;
988
+ }
989
+ function normalizeKnownAccountsCookie(request, privacy, options = {}) {
990
+ if (!privacy || privacy === "full") return null;
991
+ const raw = getKnownAccountsCookieRaw(request);
992
+ if (!raw) return null;
993
+ let accounts = {};
994
+ try {
995
+ accounts = JSON.parse(raw);
996
+ } catch {
997
+ return null;
998
+ }
999
+ let changed = false;
1000
+ for (const [userId, entry] of Object.entries(accounts)) {
1001
+ const normalized = applyPrivacyToEntry(entry, privacy);
1002
+ if (JSON.stringify(normalized) !== JSON.stringify(entry)) {
1003
+ accounts[userId] = normalized;
1004
+ changed = true;
1005
+ }
1006
+ }
1007
+ if (!changed) return null;
1008
+ const maxAge = 365 * 24 * 60 * 60;
1009
+ const secure = options.secure ?? process.env.NODE_ENV === "production";
1010
+ const sameSite = options.sameSite ?? "lax";
1011
+ const path = options.path ?? "/";
1012
+ let cookie = `${KNOWN_ACCOUNTS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(accounts))}; Path=${path}; Max-Age=${maxAge}; SameSite=${sameSite}`;
1013
+ if (secure) cookie += "; Secure";
1014
+ if (options.domain) cookie += `; Domain=${options.domain}`;
1015
+ return cookie;
1016
+ }
850
1017
 
851
1018
  // src/server/timing.ts
852
1019
  function constantTimeEqual(a, b) {
@@ -933,7 +1100,25 @@ function createAuthRoutes(config = {}) {
933
1100
  } catch {
934
1101
  return successResponse({ user: registeredUser, message: "Registration successful" }, 201);
935
1102
  }
936
- return withSession(loginData, { user: registeredUser, sessionToken: loginData.session_token, userId: registeredUser.id }, cookieOptions);
1103
+ const registerResponse = withSession(loginData, { user: registeredUser, sessionToken: loginData.session_token, userId: registeredUser.id }, cookieOptions);
1104
+ if (config.enableAccountSwitcher) {
1105
+ const existingKnown = getKnownAccountsCookieRaw(request);
1106
+ appendKnownAccountCookie(
1107
+ registerResponse.headers,
1108
+ {
1109
+ userId: registeredUser.id,
1110
+ email: registeredUser.email,
1111
+ fullName: registeredUser.full_name ?? void 0,
1112
+ avatarUrl: registeredUser.avatar_url ?? void 0,
1113
+ provider: "email",
1114
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
1115
+ },
1116
+ existingKnown,
1117
+ cookieOptions,
1118
+ config.accountSwitcherPrivacy
1119
+ );
1120
+ }
1121
+ return registerResponse;
937
1122
  }
938
1123
  // ==================== Login ====================
939
1124
  case "login": {
@@ -964,7 +1149,25 @@ function createAuthRoutes(config = {}) {
964
1149
  email: loginData.user.email
965
1150
  });
966
1151
  }
967
- return withSession(loginData, { user: loginData.user, sessionToken: loginData.session_token, userId: loginData.user.id }, cookieOptions);
1152
+ const loginResponse = withSession(loginData, { user: loginData.user, sessionToken: loginData.session_token, userId: loginData.user.id }, cookieOptions);
1153
+ if (config.enableAccountSwitcher) {
1154
+ const existingKnown = getKnownAccountsCookieRaw(request);
1155
+ appendKnownAccountCookie(
1156
+ loginResponse.headers,
1157
+ {
1158
+ userId: loginData.user.id,
1159
+ email: loginData.user.email,
1160
+ fullName: loginData.user.full_name ?? void 0,
1161
+ avatarUrl: loginData.user.avatar_url ?? void 0,
1162
+ provider: "email",
1163
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
1164
+ },
1165
+ existingKnown,
1166
+ cookieOptions,
1167
+ config.accountSwitcherPrivacy
1168
+ );
1169
+ }
1170
+ return loginResponse;
968
1171
  }
969
1172
  // ==================== Logout ====================
970
1173
  case "logout": {
@@ -1111,6 +1314,59 @@ function createAuthRoutes(config = {}) {
1111
1314
  }
1112
1315
  return successResponse({ message: "Password changed successfully" });
1113
1316
  }
1317
+ // ==================== Switch Account ====================
1318
+ // Clears the active session so the user can re-authenticate as a different account.
1319
+ // The known accounts cookie is preserved — only the session cookie is cleared.
1320
+ case "switch-account": {
1321
+ if (!config.enableAccountSwitcher) {
1322
+ return errorResponse("NOT_FOUND", "Account switcher not enabled", 404);
1323
+ }
1324
+ const session = await getSession();
1325
+ if (session) {
1326
+ try {
1327
+ await sm.auth.logout(session.sessionToken);
1328
+ } catch {
1329
+ }
1330
+ }
1331
+ const switchResponse = clearSession({ message: "Session cleared for account switch" }, cookieOptions);
1332
+ const knownAccounts = getKnownAccountsFromRequest(request);
1333
+ return new Response(
1334
+ JSON.stringify({ success: true, data: { message: "Session cleared for account switch", knownAccounts } }),
1335
+ { status: 200, headers: switchResponse.headers }
1336
+ );
1337
+ }
1338
+ // ==================== Forget Account ====================
1339
+ // Remove a specific account from the known accounts cookie.
1340
+ case "forget-account": {
1341
+ if (!config.enableAccountSwitcher) {
1342
+ return errorResponse("NOT_FOUND", "Account switcher not enabled", 404);
1343
+ }
1344
+ const { user_id } = body;
1345
+ if (!user_id) {
1346
+ return errorResponse("VALIDATION_ERROR", "user_id required", 400);
1347
+ }
1348
+ const headers = new Headers();
1349
+ headers.set("Content-Type", "application/json");
1350
+ const existingKnown = getKnownAccountsCookieRaw(request);
1351
+ removeKnownAccountFromCookie(headers, user_id, existingKnown, cookieOptions);
1352
+ return new Response(
1353
+ JSON.stringify({ success: true, data: { message: "Account forgotten" } }),
1354
+ { status: 200, headers }
1355
+ );
1356
+ }
1357
+ // ==================== Forget All Accounts ====================
1358
+ case "forget-all-accounts": {
1359
+ if (!config.enableAccountSwitcher) {
1360
+ return errorResponse("NOT_FOUND", "Account switcher not enabled", 404);
1361
+ }
1362
+ const headers = new Headers();
1363
+ headers.set("Content-Type", "application/json");
1364
+ clearKnownAccountsCookie(headers, cookieOptions);
1365
+ return new Response(
1366
+ JSON.stringify({ success: true, data: { message: "All accounts forgotten" } }),
1367
+ { status: 200, headers }
1368
+ );
1369
+ }
1114
1370
  default:
1115
1371
  return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
1116
1372
  }
@@ -1126,20 +1382,25 @@ function createAuthRoutes(config = {}) {
1126
1382
  switch (path) {
1127
1383
  // ==================== Get Current User ====================
1128
1384
  case "me": {
1385
+ const normCookie = config.enableAccountSwitcher ? normalizeKnownAccountsCookie(request, config.accountSwitcherPrivacy, cookieOptions) : null;
1386
+ const withNorm = (resp) => {
1387
+ if (normCookie) resp.headers.append("Set-Cookie", normCookie);
1388
+ return resp;
1389
+ };
1129
1390
  const session = await getSession();
1130
1391
  if (!session) {
1131
- return errorResponse("UNAUTHORIZED", "Authentication required", 401);
1392
+ return withNorm(errorResponse("UNAUTHORIZED", "Authentication required", 401));
1132
1393
  }
1133
1394
  let userData;
1134
1395
  try {
1135
1396
  userData = await sm.auth.me(session.sessionToken);
1136
1397
  } catch {
1137
- return clearSession(
1398
+ return withNorm(clearSession(
1138
1399
  { error: { code: "SESSION_EXPIRED", message: "Session expired" } },
1139
1400
  cookieOptions
1140
- );
1401
+ ));
1141
1402
  }
1142
- return successResponse({ user: userData, sessionToken: session.sessionToken, userId: session.userId });
1403
+ return withNorm(successResponse({ user: userData, sessionToken: session.sessionToken, userId: session.userId }));
1143
1404
  }
1144
1405
  // ==================== Get Session Status ====================
1145
1406
  case "session": {
@@ -1149,6 +1410,14 @@ function createAuthRoutes(config = {}) {
1149
1410
  userId: session?.userId || null
1150
1411
  });
1151
1412
  }
1413
+ // ==================== Get Known Accounts ====================
1414
+ case "known-accounts": {
1415
+ if (!config.enableAccountSwitcher) {
1416
+ return errorResponse("NOT_FOUND", "Account switcher not enabled", 404);
1417
+ }
1418
+ const knownAccounts = getKnownAccountsFromRequest(request);
1419
+ return successResponse({ knownAccounts });
1420
+ }
1152
1421
  default:
1153
1422
  return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
1154
1423
  }
@@ -1,3 +1,4 @@
1
+ import { createMoneyClient } from '@scalemule/money';
1
2
  import { cookies } from 'next/headers';
2
3
  import 'next/server';
3
4
 
@@ -687,6 +688,14 @@ var ScaleMuleServer = class {
687
688
  this.apiKey = config.apiKey;
688
689
  this.gatewayUrl = resolveGatewayUrl(config);
689
690
  this.debug = config.debug || false;
691
+ this.money = createMoneyClient({
692
+ apiKey: this.apiKey,
693
+ gatewayUrl: this.gatewayUrl,
694
+ fetch: globalThis.fetch.bind(globalThis)
695
+ });
696
+ }
697
+ moneyWithSession(sessionToken) {
698
+ return this.money.withAccessToken(sessionToken);
690
699
  }
691
700
  /**
692
701
  * Make a request to the ScaleMule API
@@ -765,6 +774,7 @@ function createServerClient(config) {
765
774
  }
766
775
  var SESSION_COOKIE_NAME = "sm_session";
767
776
  var USER_ID_COOKIE_NAME = "sm_user_id";
777
+ var KNOWN_ACCOUNTS_COOKIE_NAME = "sm_known_accounts";
768
778
  ({
769
779
  secure: process.env.NODE_ENV === "production"});
770
780
  function createCookieHeader(name, value, options = {}) {
@@ -845,6 +855,163 @@ async function getSession() {
845
855
  // Note: actual expiry is managed by ScaleMule backend
846
856
  };
847
857
  }
858
+ var MAX_KNOWN_ACCOUNTS = 10;
859
+ function maskEmail(email) {
860
+ const [local, domain] = email.split("@");
861
+ if (!domain) return "***@***.***";
862
+ const tldDot = domain.lastIndexOf(".");
863
+ const tld = tldDot > 0 ? domain.slice(tldDot) : "";
864
+ const domainBase = tldDot > 0 ? domain.slice(0, tldDot) : domain;
865
+ return `${local[0] || "*"}***@${domainBase[0] || "*"}***${tld}`;
866
+ }
867
+ function stableColorIndex(userId) {
868
+ let hash = 0;
869
+ for (let i = 0; i < userId.length; i++) {
870
+ hash = (hash << 5) - hash + userId.charCodeAt(i) | 0;
871
+ }
872
+ return Math.abs(hash) % 8;
873
+ }
874
+ function applyPrivacyToEntry(entry, privacy) {
875
+ switch (privacy) {
876
+ case "full":
877
+ return entry;
878
+ case "masked":
879
+ return {
880
+ userId: entry.userId,
881
+ email: entry.email ? maskEmail(entry.email) : void 0,
882
+ fullName: entry.fullName ? `${entry.fullName[0].toUpperCase()}.` : void 0,
883
+ provider: entry.provider,
884
+ lastActiveAt: entry.lastActiveAt,
885
+ colorIndex: stableColorIndex(entry.userId)
886
+ };
887
+ case "minimal":
888
+ return {
889
+ userId: entry.userId,
890
+ provider: entry.provider,
891
+ lastActiveAt: entry.lastActiveAt,
892
+ displayLabel: "Account",
893
+ colorIndex: stableColorIndex(entry.userId)
894
+ };
895
+ }
896
+ }
897
+ function appendKnownAccountCookie(headers, account, existingCookie, options = {}, privacy) {
898
+ let accounts = {};
899
+ if (existingCookie) {
900
+ try {
901
+ accounts = JSON.parse(decodeURIComponent(existingCookie));
902
+ } catch {
903
+ }
904
+ }
905
+ const effectivePrivacy = privacy || "full";
906
+ for (const [userId, entry] of Object.entries(accounts)) {
907
+ accounts[userId] = applyPrivacyToEntry(entry, effectivePrivacy);
908
+ }
909
+ accounts[account.userId] = applyPrivacyToEntry(account, effectivePrivacy);
910
+ const entries = Object.entries(accounts);
911
+ if (entries.length > MAX_KNOWN_ACCOUNTS) {
912
+ entries.sort((a, b) => new Date(b[1].lastActiveAt).getTime() - new Date(a[1].lastActiveAt).getTime());
913
+ accounts = Object.fromEntries(entries.slice(0, MAX_KNOWN_ACCOUNTS));
914
+ }
915
+ const maxAge = 365 * 24 * 60 * 60;
916
+ const secure = options.secure ?? process.env.NODE_ENV === "production";
917
+ const sameSite = options.sameSite ?? "lax";
918
+ const path = options.path ?? "/";
919
+ let cookie = `${KNOWN_ACCOUNTS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(accounts))}; Path=${path}; Max-Age=${maxAge}; SameSite=${sameSite}`;
920
+ if (secure) {
921
+ cookie += "; Secure";
922
+ }
923
+ if (options.domain) {
924
+ cookie += `; Domain=${options.domain}`;
925
+ }
926
+ headers.append("Set-Cookie", cookie);
927
+ }
928
+ function removeKnownAccountFromCookie(headers, userId, existingCookie, options = {}) {
929
+ let accounts = {};
930
+ if (existingCookie) {
931
+ try {
932
+ accounts = JSON.parse(decodeURIComponent(existingCookie));
933
+ } catch {
934
+ }
935
+ }
936
+ delete accounts[userId];
937
+ const maxAge = 365 * 24 * 60 * 60;
938
+ const secure = options.secure ?? process.env.NODE_ENV === "production";
939
+ const sameSite = options.sameSite ?? "lax";
940
+ const path = options.path ?? "/";
941
+ let cookie = `${KNOWN_ACCOUNTS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(accounts))}; Path=${path}; Max-Age=${maxAge}; SameSite=${sameSite}`;
942
+ if (secure) {
943
+ cookie += "; Secure";
944
+ }
945
+ if (options.domain) {
946
+ cookie += `; Domain=${options.domain}`;
947
+ }
948
+ headers.append("Set-Cookie", cookie);
949
+ }
950
+ function clearKnownAccountsCookie(headers, options = {}) {
951
+ const path = options.path ?? "/";
952
+ let cookie = `${KNOWN_ACCOUNTS_COOKIE_NAME}=; Path=${path}; Max-Age=0`;
953
+ if (options.domain) {
954
+ cookie += `; Domain=${options.domain}`;
955
+ }
956
+ headers.append("Set-Cookie", cookie);
957
+ }
958
+ function getKnownAccountsFromRequest(request) {
959
+ const cookieHeader = request.headers.get("cookie");
960
+ if (!cookieHeader) return [];
961
+ const cookies3 = Object.fromEntries(
962
+ cookieHeader.split(";").map((c) => {
963
+ const [key, ...rest] = c.trim().split("=");
964
+ return [key, decodeURIComponent(rest.join("="))];
965
+ })
966
+ );
967
+ const raw = cookies3[KNOWN_ACCOUNTS_COOKIE_NAME];
968
+ if (!raw) return [];
969
+ try {
970
+ const accounts = JSON.parse(raw);
971
+ return Object.values(accounts);
972
+ } catch {
973
+ return [];
974
+ }
975
+ }
976
+ function getKnownAccountsCookieRaw(request) {
977
+ const cookieHeader = request.headers.get("cookie");
978
+ if (!cookieHeader) return null;
979
+ const cookies3 = Object.fromEntries(
980
+ cookieHeader.split(";").map((c) => {
981
+ const [key, ...rest] = c.trim().split("=");
982
+ return [key, decodeURIComponent(rest.join("="))];
983
+ })
984
+ );
985
+ return cookies3[KNOWN_ACCOUNTS_COOKIE_NAME] || null;
986
+ }
987
+ function normalizeKnownAccountsCookie(request, privacy, options = {}) {
988
+ if (!privacy || privacy === "full") return null;
989
+ const raw = getKnownAccountsCookieRaw(request);
990
+ if (!raw) return null;
991
+ let accounts = {};
992
+ try {
993
+ accounts = JSON.parse(raw);
994
+ } catch {
995
+ return null;
996
+ }
997
+ let changed = false;
998
+ for (const [userId, entry] of Object.entries(accounts)) {
999
+ const normalized = applyPrivacyToEntry(entry, privacy);
1000
+ if (JSON.stringify(normalized) !== JSON.stringify(entry)) {
1001
+ accounts[userId] = normalized;
1002
+ changed = true;
1003
+ }
1004
+ }
1005
+ if (!changed) return null;
1006
+ const maxAge = 365 * 24 * 60 * 60;
1007
+ const secure = options.secure ?? process.env.NODE_ENV === "production";
1008
+ const sameSite = options.sameSite ?? "lax";
1009
+ const path = options.path ?? "/";
1010
+ let cookie = `${KNOWN_ACCOUNTS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(accounts))}; Path=${path}; Max-Age=${maxAge}; SameSite=${sameSite}`;
1011
+ if (secure) cookie += "; Secure";
1012
+ if (options.domain) cookie += `; Domain=${options.domain}`;
1013
+ return cookie;
1014
+ }
848
1015
 
849
1016
  // src/server/timing.ts
850
1017
  function constantTimeEqual(a, b) {
@@ -931,7 +1098,25 @@ function createAuthRoutes(config = {}) {
931
1098
  } catch {
932
1099
  return successResponse({ user: registeredUser, message: "Registration successful" }, 201);
933
1100
  }
934
- return withSession(loginData, { user: registeredUser, sessionToken: loginData.session_token, userId: registeredUser.id }, cookieOptions);
1101
+ const registerResponse = withSession(loginData, { user: registeredUser, sessionToken: loginData.session_token, userId: registeredUser.id }, cookieOptions);
1102
+ if (config.enableAccountSwitcher) {
1103
+ const existingKnown = getKnownAccountsCookieRaw(request);
1104
+ appendKnownAccountCookie(
1105
+ registerResponse.headers,
1106
+ {
1107
+ userId: registeredUser.id,
1108
+ email: registeredUser.email,
1109
+ fullName: registeredUser.full_name ?? void 0,
1110
+ avatarUrl: registeredUser.avatar_url ?? void 0,
1111
+ provider: "email",
1112
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
1113
+ },
1114
+ existingKnown,
1115
+ cookieOptions,
1116
+ config.accountSwitcherPrivacy
1117
+ );
1118
+ }
1119
+ return registerResponse;
935
1120
  }
936
1121
  // ==================== Login ====================
937
1122
  case "login": {
@@ -962,7 +1147,25 @@ function createAuthRoutes(config = {}) {
962
1147
  email: loginData.user.email
963
1148
  });
964
1149
  }
965
- return withSession(loginData, { user: loginData.user, sessionToken: loginData.session_token, userId: loginData.user.id }, cookieOptions);
1150
+ const loginResponse = withSession(loginData, { user: loginData.user, sessionToken: loginData.session_token, userId: loginData.user.id }, cookieOptions);
1151
+ if (config.enableAccountSwitcher) {
1152
+ const existingKnown = getKnownAccountsCookieRaw(request);
1153
+ appendKnownAccountCookie(
1154
+ loginResponse.headers,
1155
+ {
1156
+ userId: loginData.user.id,
1157
+ email: loginData.user.email,
1158
+ fullName: loginData.user.full_name ?? void 0,
1159
+ avatarUrl: loginData.user.avatar_url ?? void 0,
1160
+ provider: "email",
1161
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
1162
+ },
1163
+ existingKnown,
1164
+ cookieOptions,
1165
+ config.accountSwitcherPrivacy
1166
+ );
1167
+ }
1168
+ return loginResponse;
966
1169
  }
967
1170
  // ==================== Logout ====================
968
1171
  case "logout": {
@@ -1109,6 +1312,59 @@ function createAuthRoutes(config = {}) {
1109
1312
  }
1110
1313
  return successResponse({ message: "Password changed successfully" });
1111
1314
  }
1315
+ // ==================== Switch Account ====================
1316
+ // Clears the active session so the user can re-authenticate as a different account.
1317
+ // The known accounts cookie is preserved — only the session cookie is cleared.
1318
+ case "switch-account": {
1319
+ if (!config.enableAccountSwitcher) {
1320
+ return errorResponse("NOT_FOUND", "Account switcher not enabled", 404);
1321
+ }
1322
+ const session = await getSession();
1323
+ if (session) {
1324
+ try {
1325
+ await sm.auth.logout(session.sessionToken);
1326
+ } catch {
1327
+ }
1328
+ }
1329
+ const switchResponse = clearSession({ message: "Session cleared for account switch" }, cookieOptions);
1330
+ const knownAccounts = getKnownAccountsFromRequest(request);
1331
+ return new Response(
1332
+ JSON.stringify({ success: true, data: { message: "Session cleared for account switch", knownAccounts } }),
1333
+ { status: 200, headers: switchResponse.headers }
1334
+ );
1335
+ }
1336
+ // ==================== Forget Account ====================
1337
+ // Remove a specific account from the known accounts cookie.
1338
+ case "forget-account": {
1339
+ if (!config.enableAccountSwitcher) {
1340
+ return errorResponse("NOT_FOUND", "Account switcher not enabled", 404);
1341
+ }
1342
+ const { user_id } = body;
1343
+ if (!user_id) {
1344
+ return errorResponse("VALIDATION_ERROR", "user_id required", 400);
1345
+ }
1346
+ const headers = new Headers();
1347
+ headers.set("Content-Type", "application/json");
1348
+ const existingKnown = getKnownAccountsCookieRaw(request);
1349
+ removeKnownAccountFromCookie(headers, user_id, existingKnown, cookieOptions);
1350
+ return new Response(
1351
+ JSON.stringify({ success: true, data: { message: "Account forgotten" } }),
1352
+ { status: 200, headers }
1353
+ );
1354
+ }
1355
+ // ==================== Forget All Accounts ====================
1356
+ case "forget-all-accounts": {
1357
+ if (!config.enableAccountSwitcher) {
1358
+ return errorResponse("NOT_FOUND", "Account switcher not enabled", 404);
1359
+ }
1360
+ const headers = new Headers();
1361
+ headers.set("Content-Type", "application/json");
1362
+ clearKnownAccountsCookie(headers, cookieOptions);
1363
+ return new Response(
1364
+ JSON.stringify({ success: true, data: { message: "All accounts forgotten" } }),
1365
+ { status: 200, headers }
1366
+ );
1367
+ }
1112
1368
  default:
1113
1369
  return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
1114
1370
  }
@@ -1124,20 +1380,25 @@ function createAuthRoutes(config = {}) {
1124
1380
  switch (path) {
1125
1381
  // ==================== Get Current User ====================
1126
1382
  case "me": {
1383
+ const normCookie = config.enableAccountSwitcher ? normalizeKnownAccountsCookie(request, config.accountSwitcherPrivacy, cookieOptions) : null;
1384
+ const withNorm = (resp) => {
1385
+ if (normCookie) resp.headers.append("Set-Cookie", normCookie);
1386
+ return resp;
1387
+ };
1127
1388
  const session = await getSession();
1128
1389
  if (!session) {
1129
- return errorResponse("UNAUTHORIZED", "Authentication required", 401);
1390
+ return withNorm(errorResponse("UNAUTHORIZED", "Authentication required", 401));
1130
1391
  }
1131
1392
  let userData;
1132
1393
  try {
1133
1394
  userData = await sm.auth.me(session.sessionToken);
1134
1395
  } catch {
1135
- return clearSession(
1396
+ return withNorm(clearSession(
1136
1397
  { error: { code: "SESSION_EXPIRED", message: "Session expired" } },
1137
1398
  cookieOptions
1138
- );
1399
+ ));
1139
1400
  }
1140
- return successResponse({ user: userData, sessionToken: session.sessionToken, userId: session.userId });
1401
+ return withNorm(successResponse({ user: userData, sessionToken: session.sessionToken, userId: session.userId }));
1141
1402
  }
1142
1403
  // ==================== Get Session Status ====================
1143
1404
  case "session": {
@@ -1147,6 +1408,14 @@ function createAuthRoutes(config = {}) {
1147
1408
  userId: session?.userId || null
1148
1409
  });
1149
1410
  }
1411
+ // ==================== Get Known Accounts ====================
1412
+ case "known-accounts": {
1413
+ if (!config.enableAccountSwitcher) {
1414
+ return errorResponse("NOT_FOUND", "Account switcher not enabled", 404);
1415
+ }
1416
+ const knownAccounts = getKnownAccountsFromRequest(request);
1417
+ return successResponse({ knownAccounts });
1418
+ }
1150
1419
  default:
1151
1420
  return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
1152
1421
  }