@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
  var server = require('next/server');
5
6
  var crypto$1 = require('crypto');
@@ -2667,6 +2668,14 @@ var ScaleMuleServer = class {
2667
2668
  this.apiKey = config.apiKey;
2668
2669
  this.gatewayUrl = resolveGatewayUrl(config);
2669
2670
  this.debug = config.debug || false;
2671
+ this.money = money.createMoneyClient({
2672
+ apiKey: this.apiKey,
2673
+ gatewayUrl: this.gatewayUrl,
2674
+ fetch: globalThis.fetch.bind(globalThis)
2675
+ });
2676
+ }
2677
+ moneyWithSession(sessionToken) {
2678
+ return this.money.withAccessToken(sessionToken);
2670
2679
  }
2671
2680
  /**
2672
2681
  * Make a request to the ScaleMule API
@@ -2745,6 +2754,7 @@ function createServerClient(config) {
2745
2754
  }
2746
2755
  var SESSION_COOKIE_NAME = "sm_session";
2747
2756
  var USER_ID_COOKIE_NAME = "sm_user_id";
2757
+ var KNOWN_ACCOUNTS_COOKIE_NAME = "sm_known_accounts";
2748
2758
  ({
2749
2759
  secure: process.env.NODE_ENV === "production"});
2750
2760
  function createCookieHeader(name, value, options = {}) {
@@ -2845,6 +2855,163 @@ function getSessionFromRequest(request) {
2845
2855
  expiresAt: /* @__PURE__ */ new Date()
2846
2856
  };
2847
2857
  }
2858
+ var MAX_KNOWN_ACCOUNTS = 10;
2859
+ function maskEmail(email) {
2860
+ const [local, domain] = email.split("@");
2861
+ if (!domain) return "***@***.***";
2862
+ const tldDot = domain.lastIndexOf(".");
2863
+ const tld = tldDot > 0 ? domain.slice(tldDot) : "";
2864
+ const domainBase = tldDot > 0 ? domain.slice(0, tldDot) : domain;
2865
+ return `${local[0] || "*"}***@${domainBase[0] || "*"}***${tld}`;
2866
+ }
2867
+ function stableColorIndex(userId) {
2868
+ let hash = 0;
2869
+ for (let i = 0; i < userId.length; i++) {
2870
+ hash = (hash << 5) - hash + userId.charCodeAt(i) | 0;
2871
+ }
2872
+ return Math.abs(hash) % 8;
2873
+ }
2874
+ function applyPrivacyToEntry(entry, privacy) {
2875
+ switch (privacy) {
2876
+ case "full":
2877
+ return entry;
2878
+ case "masked":
2879
+ return {
2880
+ userId: entry.userId,
2881
+ email: entry.email ? maskEmail(entry.email) : void 0,
2882
+ fullName: entry.fullName ? `${entry.fullName[0].toUpperCase()}.` : void 0,
2883
+ provider: entry.provider,
2884
+ lastActiveAt: entry.lastActiveAt,
2885
+ colorIndex: stableColorIndex(entry.userId)
2886
+ };
2887
+ case "minimal":
2888
+ return {
2889
+ userId: entry.userId,
2890
+ provider: entry.provider,
2891
+ lastActiveAt: entry.lastActiveAt,
2892
+ displayLabel: "Account",
2893
+ colorIndex: stableColorIndex(entry.userId)
2894
+ };
2895
+ }
2896
+ }
2897
+ function appendKnownAccountCookie(headers2, account, existingCookie, options = {}, privacy) {
2898
+ let accounts = {};
2899
+ if (existingCookie) {
2900
+ try {
2901
+ accounts = JSON.parse(decodeURIComponent(existingCookie));
2902
+ } catch {
2903
+ }
2904
+ }
2905
+ const effectivePrivacy = privacy || "full";
2906
+ for (const [userId, entry] of Object.entries(accounts)) {
2907
+ accounts[userId] = applyPrivacyToEntry(entry, effectivePrivacy);
2908
+ }
2909
+ accounts[account.userId] = applyPrivacyToEntry(account, effectivePrivacy);
2910
+ const entries = Object.entries(accounts);
2911
+ if (entries.length > MAX_KNOWN_ACCOUNTS) {
2912
+ entries.sort((a, b) => new Date(b[1].lastActiveAt).getTime() - new Date(a[1].lastActiveAt).getTime());
2913
+ accounts = Object.fromEntries(entries.slice(0, MAX_KNOWN_ACCOUNTS));
2914
+ }
2915
+ const maxAge = 365 * 24 * 60 * 60;
2916
+ const secure = options.secure ?? process.env.NODE_ENV === "production";
2917
+ const sameSite = options.sameSite ?? "lax";
2918
+ const path = options.path ?? "/";
2919
+ let cookie = `${KNOWN_ACCOUNTS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(accounts))}; Path=${path}; Max-Age=${maxAge}; SameSite=${sameSite}`;
2920
+ if (secure) {
2921
+ cookie += "; Secure";
2922
+ }
2923
+ if (options.domain) {
2924
+ cookie += `; Domain=${options.domain}`;
2925
+ }
2926
+ headers2.append("Set-Cookie", cookie);
2927
+ }
2928
+ function removeKnownAccountFromCookie(headers2, userId, existingCookie, options = {}) {
2929
+ let accounts = {};
2930
+ if (existingCookie) {
2931
+ try {
2932
+ accounts = JSON.parse(decodeURIComponent(existingCookie));
2933
+ } catch {
2934
+ }
2935
+ }
2936
+ delete accounts[userId];
2937
+ const maxAge = 365 * 24 * 60 * 60;
2938
+ const secure = options.secure ?? process.env.NODE_ENV === "production";
2939
+ const sameSite = options.sameSite ?? "lax";
2940
+ const path = options.path ?? "/";
2941
+ let cookie = `${KNOWN_ACCOUNTS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(accounts))}; Path=${path}; Max-Age=${maxAge}; SameSite=${sameSite}`;
2942
+ if (secure) {
2943
+ cookie += "; Secure";
2944
+ }
2945
+ if (options.domain) {
2946
+ cookie += `; Domain=${options.domain}`;
2947
+ }
2948
+ headers2.append("Set-Cookie", cookie);
2949
+ }
2950
+ function clearKnownAccountsCookie(headers2, options = {}) {
2951
+ const path = options.path ?? "/";
2952
+ let cookie = `${KNOWN_ACCOUNTS_COOKIE_NAME}=; Path=${path}; Max-Age=0`;
2953
+ if (options.domain) {
2954
+ cookie += `; Domain=${options.domain}`;
2955
+ }
2956
+ headers2.append("Set-Cookie", cookie);
2957
+ }
2958
+ function getKnownAccountsFromRequest(request) {
2959
+ const cookieHeader = request.headers.get("cookie");
2960
+ if (!cookieHeader) return [];
2961
+ const cookies4 = Object.fromEntries(
2962
+ cookieHeader.split(";").map((c) => {
2963
+ const [key, ...rest] = c.trim().split("=");
2964
+ return [key, decodeURIComponent(rest.join("="))];
2965
+ })
2966
+ );
2967
+ const raw = cookies4[KNOWN_ACCOUNTS_COOKIE_NAME];
2968
+ if (!raw) return [];
2969
+ try {
2970
+ const accounts = JSON.parse(raw);
2971
+ return Object.values(accounts);
2972
+ } catch {
2973
+ return [];
2974
+ }
2975
+ }
2976
+ function getKnownAccountsCookieRaw(request) {
2977
+ const cookieHeader = request.headers.get("cookie");
2978
+ if (!cookieHeader) return null;
2979
+ const cookies4 = Object.fromEntries(
2980
+ cookieHeader.split(";").map((c) => {
2981
+ const [key, ...rest] = c.trim().split("=");
2982
+ return [key, decodeURIComponent(rest.join("="))];
2983
+ })
2984
+ );
2985
+ return cookies4[KNOWN_ACCOUNTS_COOKIE_NAME] || null;
2986
+ }
2987
+ function normalizeKnownAccountsCookie(request, privacy, options = {}) {
2988
+ if (!privacy || privacy === "full") return null;
2989
+ const raw = getKnownAccountsCookieRaw(request);
2990
+ if (!raw) return null;
2991
+ let accounts = {};
2992
+ try {
2993
+ accounts = JSON.parse(raw);
2994
+ } catch {
2995
+ return null;
2996
+ }
2997
+ let changed = false;
2998
+ for (const [userId, entry] of Object.entries(accounts)) {
2999
+ const normalized = applyPrivacyToEntry(entry, privacy);
3000
+ if (JSON.stringify(normalized) !== JSON.stringify(entry)) {
3001
+ accounts[userId] = normalized;
3002
+ changed = true;
3003
+ }
3004
+ }
3005
+ if (!changed) return null;
3006
+ const maxAge = 365 * 24 * 60 * 60;
3007
+ const secure = options.secure ?? process.env.NODE_ENV === "production";
3008
+ const sameSite = options.sameSite ?? "lax";
3009
+ const path = options.path ?? "/";
3010
+ let cookie = `${KNOWN_ACCOUNTS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(accounts))}; Path=${path}; Max-Age=${maxAge}; SameSite=${sameSite}`;
3011
+ if (secure) cookie += "; Secure";
3012
+ if (options.domain) cookie += `; Domain=${options.domain}`;
3013
+ return cookie;
3014
+ }
2848
3015
  async function requireSession() {
2849
3016
  const session = await getSession();
2850
3017
  if (!session) {
@@ -3004,7 +3171,25 @@ function createAuthRoutes(config = {}) {
3004
3171
  } catch {
3005
3172
  return successResponse({ user: registeredUser, message: "Registration successful" }, 201);
3006
3173
  }
3007
- return withSession(loginData, { user: registeredUser, sessionToken: loginData.session_token, userId: registeredUser.id }, cookieOptions);
3174
+ const registerResponse = withSession(loginData, { user: registeredUser, sessionToken: loginData.session_token, userId: registeredUser.id }, cookieOptions);
3175
+ if (config.enableAccountSwitcher) {
3176
+ const existingKnown = getKnownAccountsCookieRaw(request);
3177
+ appendKnownAccountCookie(
3178
+ registerResponse.headers,
3179
+ {
3180
+ userId: registeredUser.id,
3181
+ email: registeredUser.email,
3182
+ fullName: registeredUser.full_name ?? void 0,
3183
+ avatarUrl: registeredUser.avatar_url ?? void 0,
3184
+ provider: "email",
3185
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
3186
+ },
3187
+ existingKnown,
3188
+ cookieOptions,
3189
+ config.accountSwitcherPrivacy
3190
+ );
3191
+ }
3192
+ return registerResponse;
3008
3193
  }
3009
3194
  // ==================== Login ====================
3010
3195
  case "login": {
@@ -3035,7 +3220,25 @@ function createAuthRoutes(config = {}) {
3035
3220
  email: loginData.user.email
3036
3221
  });
3037
3222
  }
3038
- return withSession(loginData, { user: loginData.user, sessionToken: loginData.session_token, userId: loginData.user.id }, cookieOptions);
3223
+ const loginResponse = withSession(loginData, { user: loginData.user, sessionToken: loginData.session_token, userId: loginData.user.id }, cookieOptions);
3224
+ if (config.enableAccountSwitcher) {
3225
+ const existingKnown = getKnownAccountsCookieRaw(request);
3226
+ appendKnownAccountCookie(
3227
+ loginResponse.headers,
3228
+ {
3229
+ userId: loginData.user.id,
3230
+ email: loginData.user.email,
3231
+ fullName: loginData.user.full_name ?? void 0,
3232
+ avatarUrl: loginData.user.avatar_url ?? void 0,
3233
+ provider: "email",
3234
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
3235
+ },
3236
+ existingKnown,
3237
+ cookieOptions,
3238
+ config.accountSwitcherPrivacy
3239
+ );
3240
+ }
3241
+ return loginResponse;
3039
3242
  }
3040
3243
  // ==================== Logout ====================
3041
3244
  case "logout": {
@@ -3182,6 +3385,59 @@ function createAuthRoutes(config = {}) {
3182
3385
  }
3183
3386
  return successResponse({ message: "Password changed successfully" });
3184
3387
  }
3388
+ // ==================== Switch Account ====================
3389
+ // Clears the active session so the user can re-authenticate as a different account.
3390
+ // The known accounts cookie is preserved — only the session cookie is cleared.
3391
+ case "switch-account": {
3392
+ if (!config.enableAccountSwitcher) {
3393
+ return errorResponse("NOT_FOUND", "Account switcher not enabled", 404);
3394
+ }
3395
+ const session = await getSession();
3396
+ if (session) {
3397
+ try {
3398
+ await sm.auth.logout(session.sessionToken);
3399
+ } catch {
3400
+ }
3401
+ }
3402
+ const switchResponse = clearSession({ message: "Session cleared for account switch" }, cookieOptions);
3403
+ const knownAccounts = getKnownAccountsFromRequest(request);
3404
+ return new Response(
3405
+ JSON.stringify({ success: true, data: { message: "Session cleared for account switch", knownAccounts } }),
3406
+ { status: 200, headers: switchResponse.headers }
3407
+ );
3408
+ }
3409
+ // ==================== Forget Account ====================
3410
+ // Remove a specific account from the known accounts cookie.
3411
+ case "forget-account": {
3412
+ if (!config.enableAccountSwitcher) {
3413
+ return errorResponse("NOT_FOUND", "Account switcher not enabled", 404);
3414
+ }
3415
+ const { user_id } = body;
3416
+ if (!user_id) {
3417
+ return errorResponse("VALIDATION_ERROR", "user_id required", 400);
3418
+ }
3419
+ const headers2 = new Headers();
3420
+ headers2.set("Content-Type", "application/json");
3421
+ const existingKnown = getKnownAccountsCookieRaw(request);
3422
+ removeKnownAccountFromCookie(headers2, user_id, existingKnown, cookieOptions);
3423
+ return new Response(
3424
+ JSON.stringify({ success: true, data: { message: "Account forgotten" } }),
3425
+ { status: 200, headers: headers2 }
3426
+ );
3427
+ }
3428
+ // ==================== Forget All Accounts ====================
3429
+ case "forget-all-accounts": {
3430
+ if (!config.enableAccountSwitcher) {
3431
+ return errorResponse("NOT_FOUND", "Account switcher not enabled", 404);
3432
+ }
3433
+ const headers2 = new Headers();
3434
+ headers2.set("Content-Type", "application/json");
3435
+ clearKnownAccountsCookie(headers2, cookieOptions);
3436
+ return new Response(
3437
+ JSON.stringify({ success: true, data: { message: "All accounts forgotten" } }),
3438
+ { status: 200, headers: headers2 }
3439
+ );
3440
+ }
3185
3441
  default:
3186
3442
  return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
3187
3443
  }
@@ -3197,20 +3453,25 @@ function createAuthRoutes(config = {}) {
3197
3453
  switch (path) {
3198
3454
  // ==================== Get Current User ====================
3199
3455
  case "me": {
3456
+ const normCookie = config.enableAccountSwitcher ? normalizeKnownAccountsCookie(request, config.accountSwitcherPrivacy, cookieOptions) : null;
3457
+ const withNorm = (resp) => {
3458
+ if (normCookie) resp.headers.append("Set-Cookie", normCookie);
3459
+ return resp;
3460
+ };
3200
3461
  const session = await getSession();
3201
3462
  if (!session) {
3202
- return errorResponse("UNAUTHORIZED", "Authentication required", 401);
3463
+ return withNorm(errorResponse("UNAUTHORIZED", "Authentication required", 401));
3203
3464
  }
3204
3465
  let userData;
3205
3466
  try {
3206
3467
  userData = await sm.auth.me(session.sessionToken);
3207
3468
  } catch {
3208
- return clearSession(
3469
+ return withNorm(clearSession(
3209
3470
  { error: { code: "SESSION_EXPIRED", message: "Session expired" } },
3210
3471
  cookieOptions
3211
- );
3472
+ ));
3212
3473
  }
3213
- return successResponse({ user: userData, sessionToken: session.sessionToken, userId: session.userId });
3474
+ return withNorm(successResponse({ user: userData, sessionToken: session.sessionToken, userId: session.userId }));
3214
3475
  }
3215
3476
  // ==================== Get Session Status ====================
3216
3477
  case "session": {
@@ -3220,6 +3481,14 @@ function createAuthRoutes(config = {}) {
3220
3481
  userId: session?.userId || null
3221
3482
  });
3222
3483
  }
3484
+ // ==================== Get Known Accounts ====================
3485
+ case "known-accounts": {
3486
+ if (!config.enableAccountSwitcher) {
3487
+ return errorResponse("NOT_FOUND", "Account switcher not enabled", 404);
3488
+ }
3489
+ const knownAccounts = getKnownAccountsFromRequest(request);
3490
+ return successResponse({ knownAccounts });
3491
+ }
3223
3492
  default:
3224
3493
  return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
3225
3494
  }
@@ -4827,16 +5096,68 @@ async function prefetchBundles(keys) {
4827
5096
  await Promise.all(keys.map((key) => getBundle(key)));
4828
5097
  }
4829
5098
 
5099
+ // src/server/redirect.ts
5100
+ function normalizeOrigin(origin) {
5101
+ try {
5102
+ const u = new URL(origin);
5103
+ return `${u.protocol}//${u.host}`.toLowerCase();
5104
+ } catch {
5105
+ return null;
5106
+ }
5107
+ }
5108
+ function isSafeSchemeRelative(input) {
5109
+ return !(input.startsWith("//") || input.startsWith("\\"));
5110
+ }
5111
+ function validateSafeRedirect(input, opts = {}) {
5112
+ const defaultPath = opts.defaultPath ?? "/";
5113
+ if (!input || typeof input !== "string") return defaultPath;
5114
+ const trimmed = input.trim();
5115
+ if (!trimmed) return defaultPath;
5116
+ if (!isSafeSchemeRelative(trimmed)) return defaultPath;
5117
+ const colon = trimmed.indexOf(":");
5118
+ if (colon > 0 && colon < 15) {
5119
+ const scheme = trimmed.slice(0, colon).toLowerCase();
5120
+ if (/^[a-z][a-z0-9+.-]*$/.test(scheme) && scheme !== "http" && scheme !== "https") {
5121
+ return defaultPath;
5122
+ }
5123
+ }
5124
+ if (trimmed.startsWith("/")) {
5125
+ if (trimmed.length > 1 && trimmed[1] === "\\") return defaultPath;
5126
+ return trimmed;
5127
+ }
5128
+ const allowed = (opts.allowedOrigins ?? []).map(normalizeOrigin).filter((o) => o !== null);
5129
+ try {
5130
+ const parsed = new URL(trimmed);
5131
+ const origin = `${parsed.protocol}//${parsed.host}`.toLowerCase();
5132
+ if (!allowed.includes(origin)) return defaultPath;
5133
+ if (opts.stripSameOriginHost !== false) {
5134
+ return `${parsed.pathname}${parsed.search}${parsed.hash}` || "/";
5135
+ }
5136
+ return trimmed;
5137
+ } catch {
5138
+ return defaultPath;
5139
+ }
5140
+ }
5141
+ function isSafeRedirect(input, opts = {}) {
5142
+ if (!input || typeof input !== "string") return false;
5143
+ const defaultPath = `__unsafe_sentinel_${Math.random()}`;
5144
+ const resolved = validateSafeRedirect(input, { ...opts, defaultPath });
5145
+ return resolved !== defaultPath;
5146
+ }
5147
+
4830
5148
  exports.CSRF_COOKIE_NAME = CSRF_COOKIE_NAME;
4831
5149
  exports.CSRF_HEADER_NAME = CSRF_HEADER_NAME;
5150
+ exports.KNOWN_ACCOUNTS_COOKIE_NAME = KNOWN_ACCOUNTS_COOKIE_NAME;
4832
5151
  exports.OAUTH_STATE_COOKIE_NAME = OAUTH_STATE_COOKIE_NAME;
4833
5152
  exports.SESSION_COOKIE_NAME = SESSION_COOKIE_NAME;
4834
5153
  exports.ScaleMuleError = ScaleMuleError;
4835
5154
  exports.ScaleMuleServer = ScaleMuleServer;
4836
5155
  exports.USER_ID_COOKIE_NAME = USER_ID_COOKIE_NAME;
4837
5156
  exports.apiHandler = apiHandler;
5157
+ exports.appendKnownAccountCookie = appendKnownAccountCookie;
4838
5158
  exports.buildClientContextHeaders = buildClientContextHeaders;
4839
5159
  exports.buildFlagContext = buildFlagContext;
5160
+ exports.clearKnownAccountsCookie = clearKnownAccountsCookie;
4840
5161
  exports.clearOAuthState = clearOAuthState;
4841
5162
  exports.clearSession = clearSession;
4842
5163
  exports.configureBundles = configureBundles;
@@ -4858,6 +5179,8 @@ exports.getAppSecretOrDefault = getAppSecretOrDefault;
4858
5179
  exports.getBootstrapFlags = getBootstrapFlags;
4859
5180
  exports.getBundle = getBundle;
4860
5181
  exports.getCSRFToken = getCSRFToken;
5182
+ exports.getKnownAccountsCookieRaw = getKnownAccountsCookieRaw;
5183
+ exports.getKnownAccountsFromRequest = getKnownAccountsFromRequest;
4861
5184
  exports.getMySqlBundle = getMySqlBundle;
4862
5185
  exports.getOAuthBundle = getOAuthBundle;
4863
5186
  exports.getPostgresBundle = getPostgresBundle;
@@ -4868,10 +5191,13 @@ exports.getSessionFromRequest = getSessionFromRequest;
4868
5191
  exports.getSmtpBundle = getSmtpBundle;
4869
5192
  exports.invalidateBundleCache = invalidateBundleCache;
4870
5193
  exports.invalidateSecretCache = invalidateSecretCache;
5194
+ exports.isSafeRedirect = isSafeRedirect;
5195
+ exports.normalizeKnownAccountsCookie = normalizeKnownAccountsCookie;
4871
5196
  exports.parseWebhookEvent = parseWebhookEvent;
4872
5197
  exports.prefetchBundles = prefetchBundles;
4873
5198
  exports.prefetchSecrets = prefetchSecrets;
4874
5199
  exports.registerVideoWebhook = registerVideoWebhook;
5200
+ exports.removeKnownAccountFromCookie = removeKnownAccountFromCookie;
4875
5201
  exports.requireAppSecret = requireAppSecret;
4876
5202
  exports.requireBundle = requireBundle;
4877
5203
  exports.requireSession = requireSession;
@@ -4882,6 +5208,7 @@ exports.validateCSRFToken = validateCSRFToken;
4882
5208
  exports.validateCSRFTokenAsync = validateCSRFTokenAsync;
4883
5209
  exports.validateOAuthState = validateOAuthState;
4884
5210
  exports.validateOAuthStateAsync = validateOAuthStateAsync;
5211
+ exports.validateSafeRedirect = validateSafeRedirect;
4885
5212
  exports.verifyWebhookSignature = verifyWebhookSignature;
4886
5213
  exports.withAuth = withAuth;
4887
5214
  exports.withCSRFProtection = withCSRFProtection;