@scalemule/nextjs 0.0.35 → 0.0.37

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