@scalemule/nextjs 0.0.39 → 0.0.41
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/client.d.mts +5 -1
- package/dist/client.d.ts +5 -1
- package/dist/{index-BoENfro3.d.mts → index-BIIUrnPr.d.mts} +54 -1
- package/dist/{index-BoENfro3.d.ts → index-BIIUrnPr.d.ts} +54 -1
- package/dist/index.d.mts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +176 -89
- package/dist/index.mjs +168 -90
- package/dist/server/auth.js +275 -6
- package/dist/server/auth.mjs +275 -6
- package/dist/server/index.d.mts +252 -4
- package/dist/server/index.d.ts +252 -4
- package/dist/server/index.js +403 -6
- package/dist/server/index.mjs +393 -7
- package/dist/server/webhook-handler.d.mts +3 -2
- package/dist/server/webhook-handler.d.ts +3 -2
- package/dist/server/webhook-handler.js +3 -0
- package/dist/server/webhook-handler.mjs +3 -0
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{webhook-handler-CjSSCdvI.d.ts → webhook-handler-BjFbqZuq.d.ts} +4 -1
- package/dist/{webhook-handler-Cz9jtet2.d.mts → webhook-handler-BqzCYRNJ.d.mts} +4 -1
- package/package.json +3 -2
package/dist/server/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,138 @@ 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
|
+
|
|
5148
|
+
// src/server/security-headers.ts
|
|
5149
|
+
var DEFAULT_PERMISSIONS_POLICY = {
|
|
5150
|
+
// Conference track features — allow self-origin only.
|
|
5151
|
+
camera: "self",
|
|
5152
|
+
microphone: "self",
|
|
5153
|
+
"display-capture": "self",
|
|
5154
|
+
// Deny-by-default for features the SDK never uses.
|
|
5155
|
+
geolocation: "()",
|
|
5156
|
+
payment: "()",
|
|
5157
|
+
"publickey-credentials-get": "self",
|
|
5158
|
+
usb: "()",
|
|
5159
|
+
serial: "()",
|
|
5160
|
+
bluetooth: "()"
|
|
5161
|
+
};
|
|
5162
|
+
function formatPermissionsDirective(feature, value) {
|
|
5163
|
+
if (value === "*") return `${feature}=*`;
|
|
5164
|
+
if (value === "()" || Array.isArray(value) && value.length === 0) {
|
|
5165
|
+
return `${feature}=()`;
|
|
5166
|
+
}
|
|
5167
|
+
if (value === "self") return `${feature}=(self)`;
|
|
5168
|
+
const origins = value;
|
|
5169
|
+
const parts = origins.map((o) => o === "self" ? "self" : `"${o}"`);
|
|
5170
|
+
return `${feature}=(${parts.join(" ")})`;
|
|
5171
|
+
}
|
|
5172
|
+
function buildSecurityHeaders(opts = {}) {
|
|
5173
|
+
const headers2 = [];
|
|
5174
|
+
const includeHsts = opts.includeHsts ?? true;
|
|
5175
|
+
const hstsMaxAge = opts.hstsMaxAgeSeconds ?? 31536e3;
|
|
5176
|
+
if (includeHsts && hstsMaxAge > 0) {
|
|
5177
|
+
const parts = [`max-age=${hstsMaxAge}`];
|
|
5178
|
+
if (opts.hstsIncludeSubDomains ?? true) parts.push("includeSubDomains");
|
|
5179
|
+
if (opts.hstsPreload ?? false) parts.push("preload");
|
|
5180
|
+
headers2.push({
|
|
5181
|
+
key: "Strict-Transport-Security",
|
|
5182
|
+
value: parts.join("; ")
|
|
5183
|
+
});
|
|
5184
|
+
}
|
|
5185
|
+
const frameOptions = opts.frameOptions === void 0 ? "DENY" : opts.frameOptions;
|
|
5186
|
+
if (frameOptions !== null) {
|
|
5187
|
+
headers2.push({ key: "X-Frame-Options", value: frameOptions });
|
|
5188
|
+
}
|
|
5189
|
+
const contentTypeOptions = opts.contentTypeOptions === void 0 ? "nosniff" : opts.contentTypeOptions;
|
|
5190
|
+
if (contentTypeOptions !== null) {
|
|
5191
|
+
headers2.push({ key: "X-Content-Type-Options", value: contentTypeOptions });
|
|
5192
|
+
}
|
|
5193
|
+
headers2.push({
|
|
5194
|
+
key: "Referrer-Policy",
|
|
5195
|
+
value: opts.referrerPolicy ?? "strict-origin-when-cross-origin"
|
|
5196
|
+
});
|
|
5197
|
+
const xss = opts.xssProtection === void 0 ? "0" : opts.xssProtection;
|
|
5198
|
+
if (xss !== null) {
|
|
5199
|
+
headers2.push({ key: "X-XSS-Protection", value: xss });
|
|
5200
|
+
}
|
|
5201
|
+
const permissions = opts.permissionsPolicy === void 0 ? DEFAULT_PERMISSIONS_POLICY : opts.permissionsPolicy;
|
|
5202
|
+
if (permissions !== null) {
|
|
5203
|
+
const directives = Object.entries(permissions).map(([feature, value]) => formatPermissionsDirective(feature, value)).join(", ");
|
|
5204
|
+
headers2.push({ key: "Permissions-Policy", value: directives });
|
|
5205
|
+
}
|
|
5206
|
+
if (opts.extraHeaders?.length) {
|
|
5207
|
+
const byKey = new Map(headers2.map((h) => [h.key.toLowerCase(), h]));
|
|
5208
|
+
for (const extra of opts.extraHeaders) {
|
|
5209
|
+
byKey.set(extra.key.toLowerCase(), extra);
|
|
5210
|
+
}
|
|
5211
|
+
return Array.from(byKey.values());
|
|
5212
|
+
}
|
|
5213
|
+
return headers2;
|
|
5214
|
+
}
|
|
5215
|
+
|
|
4830
5216
|
exports.CSRF_COOKIE_NAME = CSRF_COOKIE_NAME;
|
|
4831
5217
|
exports.CSRF_HEADER_NAME = CSRF_HEADER_NAME;
|
|
5218
|
+
exports.DEFAULT_PERMISSIONS_POLICY = DEFAULT_PERMISSIONS_POLICY;
|
|
5219
|
+
exports.KNOWN_ACCOUNTS_COOKIE_NAME = KNOWN_ACCOUNTS_COOKIE_NAME;
|
|
4832
5220
|
exports.OAUTH_STATE_COOKIE_NAME = OAUTH_STATE_COOKIE_NAME;
|
|
4833
5221
|
exports.SESSION_COOKIE_NAME = SESSION_COOKIE_NAME;
|
|
4834
5222
|
exports.ScaleMuleError = ScaleMuleError;
|
|
4835
5223
|
exports.ScaleMuleServer = ScaleMuleServer;
|
|
4836
5224
|
exports.USER_ID_COOKIE_NAME = USER_ID_COOKIE_NAME;
|
|
4837
5225
|
exports.apiHandler = apiHandler;
|
|
5226
|
+
exports.appendKnownAccountCookie = appendKnownAccountCookie;
|
|
4838
5227
|
exports.buildClientContextHeaders = buildClientContextHeaders;
|
|
4839
5228
|
exports.buildFlagContext = buildFlagContext;
|
|
5229
|
+
exports.buildSecurityHeaders = buildSecurityHeaders;
|
|
5230
|
+
exports.clearKnownAccountsCookie = clearKnownAccountsCookie;
|
|
4840
5231
|
exports.clearOAuthState = clearOAuthState;
|
|
4841
5232
|
exports.clearSession = clearSession;
|
|
4842
5233
|
exports.configureBundles = configureBundles;
|
|
@@ -4858,6 +5249,8 @@ exports.getAppSecretOrDefault = getAppSecretOrDefault;
|
|
|
4858
5249
|
exports.getBootstrapFlags = getBootstrapFlags;
|
|
4859
5250
|
exports.getBundle = getBundle;
|
|
4860
5251
|
exports.getCSRFToken = getCSRFToken;
|
|
5252
|
+
exports.getKnownAccountsCookieRaw = getKnownAccountsCookieRaw;
|
|
5253
|
+
exports.getKnownAccountsFromRequest = getKnownAccountsFromRequest;
|
|
4861
5254
|
exports.getMySqlBundle = getMySqlBundle;
|
|
4862
5255
|
exports.getOAuthBundle = getOAuthBundle;
|
|
4863
5256
|
exports.getPostgresBundle = getPostgresBundle;
|
|
@@ -4868,10 +5261,13 @@ exports.getSessionFromRequest = getSessionFromRequest;
|
|
|
4868
5261
|
exports.getSmtpBundle = getSmtpBundle;
|
|
4869
5262
|
exports.invalidateBundleCache = invalidateBundleCache;
|
|
4870
5263
|
exports.invalidateSecretCache = invalidateSecretCache;
|
|
5264
|
+
exports.isSafeRedirect = isSafeRedirect;
|
|
5265
|
+
exports.normalizeKnownAccountsCookie = normalizeKnownAccountsCookie;
|
|
4871
5266
|
exports.parseWebhookEvent = parseWebhookEvent;
|
|
4872
5267
|
exports.prefetchBundles = prefetchBundles;
|
|
4873
5268
|
exports.prefetchSecrets = prefetchSecrets;
|
|
4874
5269
|
exports.registerVideoWebhook = registerVideoWebhook;
|
|
5270
|
+
exports.removeKnownAccountFromCookie = removeKnownAccountFromCookie;
|
|
4875
5271
|
exports.requireAppSecret = requireAppSecret;
|
|
4876
5272
|
exports.requireBundle = requireBundle;
|
|
4877
5273
|
exports.requireSession = requireSession;
|
|
@@ -4882,6 +5278,7 @@ exports.validateCSRFToken = validateCSRFToken;
|
|
|
4882
5278
|
exports.validateCSRFTokenAsync = validateCSRFTokenAsync;
|
|
4883
5279
|
exports.validateOAuthState = validateOAuthState;
|
|
4884
5280
|
exports.validateOAuthStateAsync = validateOAuthStateAsync;
|
|
5281
|
+
exports.validateSafeRedirect = validateSafeRedirect;
|
|
4885
5282
|
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
4886
5283
|
exports.withAuth = withAuth;
|
|
4887
5284
|
exports.withCSRFProtection = withCSRFProtection;
|