@sylphx/sdk 0.10.3 → 0.10.4
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/README.md +6 -0
- package/dist/index.d.ts +39 -12
- package/dist/index.mjs +96 -20
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs/index.d.ts +62 -1
- package/dist/nextjs/index.mjs +220 -24
- package/dist/nextjs/index.mjs.map +1 -1
- package/dist/react/index.d.ts +6 -1
- package/dist/react/index.mjs +24 -4
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/index.d.ts +3 -1
- package/dist/server/index.mjs.map +1 -1
- package/package.json +21 -2
package/dist/nextjs/index.d.ts
CHANGED
|
@@ -126,6 +126,42 @@ interface SylphxMiddlewareConfig {
|
|
|
126
126
|
* @default '/select-organization'
|
|
127
127
|
*/
|
|
128
128
|
orgSelectionUrl?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Active organization context behaviour.
|
|
131
|
+
*
|
|
132
|
+
* Enabled by default so Next.js apps get durable org-scoped sessions without
|
|
133
|
+
* writing their own refresh/switch orchestration:
|
|
134
|
+
* - `/auth/switch-org` records the selected org in SDK-owned HttpOnly cookies
|
|
135
|
+
* - token refresh restores that org-scoped JWT when possible
|
|
136
|
+
* - single-org users are auto-scoped after refresh
|
|
137
|
+
*
|
|
138
|
+
* Apps migrating from pre-SDK org cookies can list additional cookie names
|
|
139
|
+
* for read compatibility. The SDK still writes only its own names.
|
|
140
|
+
*/
|
|
141
|
+
organizationContext?: SylphxOrganizationContextConfig;
|
|
142
|
+
}
|
|
143
|
+
interface SylphxOrganizationContextConfig {
|
|
144
|
+
/**
|
|
145
|
+
* Preserve active organization context through refresh.
|
|
146
|
+
* @default true
|
|
147
|
+
*/
|
|
148
|
+
enabled?: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* When no active org preference exists and the user has exactly one org,
|
|
151
|
+
* automatically restore an org-scoped session token after refresh.
|
|
152
|
+
* @default true
|
|
153
|
+
*/
|
|
154
|
+
autoSelectSingleOrganization?: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Additional org-id cookie names to read during migration from existing apps.
|
|
157
|
+
* The SDK-owned `__{namespace}_active_org_id` cookie is always read first.
|
|
158
|
+
*/
|
|
159
|
+
additionalOrgIdCookies?: readonly string[];
|
|
160
|
+
/**
|
|
161
|
+
* Additional org-slug cookie names to read during migration from existing apps.
|
|
162
|
+
* The SDK-owned `__{namespace}_active_org_slug` cookie is always read first.
|
|
163
|
+
*/
|
|
164
|
+
additionalOrgSlugCookies?: readonly string[];
|
|
129
165
|
}
|
|
130
166
|
/**
|
|
131
167
|
* Create Sylphx middleware — State of the Art
|
|
@@ -485,6 +521,10 @@ declare function getCookieNames(namespace: string): {
|
|
|
485
521
|
REFRESH: string;
|
|
486
522
|
/** JS-readable user data for client hydration (5 min) */
|
|
487
523
|
USER: string;
|
|
524
|
+
/** HttpOnly active organization ID used to preserve org-scoped sessions */
|
|
525
|
+
ACTIVE_ORG_ID: string;
|
|
526
|
+
/** HttpOnly active organization slug used to preserve org-scoped sessions */
|
|
527
|
+
ACTIVE_ORG_SLUG: string;
|
|
488
528
|
};
|
|
489
529
|
/**
|
|
490
530
|
* Session token lifetime (5 minutes like Clerk)
|
|
@@ -494,6 +534,13 @@ declare const SESSION_TOKEN_LIFETIME: number;
|
|
|
494
534
|
* Refresh token lifetime (30 days)
|
|
495
535
|
*/
|
|
496
536
|
declare const REFRESH_TOKEN_LIFETIME: number;
|
|
537
|
+
/**
|
|
538
|
+
* Active organization context lifetime (30 days).
|
|
539
|
+
*
|
|
540
|
+
* This matches refresh-token lifetime: the org preference is session-scoped
|
|
541
|
+
* state, not a permanent user preference. Clearing auth clears this too.
|
|
542
|
+
*/
|
|
543
|
+
declare const ACTIVE_ORG_LIFETIME: number;
|
|
497
544
|
/**
|
|
498
545
|
* Cookie options for HttpOnly tokens (session, refresh)
|
|
499
546
|
*
|
|
@@ -521,6 +568,20 @@ declare const USER_COOKIE_OPTIONS: {
|
|
|
521
568
|
sameSite: "lax";
|
|
522
569
|
path: string;
|
|
523
570
|
};
|
|
571
|
+
/**
|
|
572
|
+
* Cookie options for active organization context.
|
|
573
|
+
*
|
|
574
|
+
* Active org is not a secret, but it controls which org-scoped JWT the SDK
|
|
575
|
+
* restores after refresh. Keep it HttpOnly so browser JavaScript cannot
|
|
576
|
+
* silently steer server-side auth context outside the official switch-org
|
|
577
|
+
* route.
|
|
578
|
+
*/
|
|
579
|
+
declare const ACTIVE_ORG_COOKIE_OPTIONS: {
|
|
580
|
+
httpOnly: boolean;
|
|
581
|
+
secure: boolean;
|
|
582
|
+
sameSite: "lax";
|
|
583
|
+
path: string;
|
|
584
|
+
};
|
|
524
585
|
/**
|
|
525
586
|
* Auth cookies data returned by getAuthCookies
|
|
526
587
|
*/
|
|
@@ -588,4 +649,4 @@ declare function clearAuthCookiesMiddleware(response: NextResponse, namespace: s
|
|
|
588
649
|
*/
|
|
589
650
|
declare function parseUserCookie(value: string): UserCookieData | null;
|
|
590
651
|
|
|
591
|
-
export { type AuthCookiesData, type AuthResult, REFRESH_TOKEN_LIFETIME, SECURE_COOKIE_OPTIONS, SESSION_TOKEN_LIFETIME, SESSION_TOKEN_LIFETIME_MS, type SylphxMiddlewareConfig, TOKEN_EXPIRY_BUFFER_MS, USER_COOKIE_OPTIONS, type UserCookieData, auth, clearAuthCookies, clearAuthCookiesMiddleware, configureServer, createMatcher, createSylphxMiddleware, currentUser, currentUserId, decodeUserId, encodeUserId, getAuthCookies, getAuthorizationUrl, getCookieNames, getNamespace, getSessionToken, handleCallback, hasRefreshToken, isSessionExpired, parseUserCookie, setAuthCookies, setAuthCookiesMiddleware, signOut, sylphxMiddleware, syncAuthToCookies };
|
|
652
|
+
export { ACTIVE_ORG_COOKIE_OPTIONS, ACTIVE_ORG_LIFETIME, type AuthCookiesData, type AuthResult, REFRESH_TOKEN_LIFETIME, SECURE_COOKIE_OPTIONS, SESSION_TOKEN_LIFETIME, SESSION_TOKEN_LIFETIME_MS, type SylphxMiddlewareConfig, type SylphxOrganizationContextConfig, TOKEN_EXPIRY_BUFFER_MS, USER_COOKIE_OPTIONS, type UserCookieData, auth, clearAuthCookies, clearAuthCookiesMiddleware, configureServer, createMatcher, createSylphxMiddleware, currentUser, currentUserId, decodeUserId, encodeUserId, getAuthCookies, getAuthorizationUrl, getCookieNames, getNamespace, getSessionToken, handleCallback, hasRefreshToken, isSessionExpired, parseUserCookie, setAuthCookies, setAuthCookiesMiddleware, signOut, sylphxMiddleware, syncAuthToCookies };
|
package/dist/nextjs/index.mjs
CHANGED
|
@@ -189,11 +189,16 @@ function getCookieNames(namespace) {
|
|
|
189
189
|
/** HttpOnly refresh token (30 days) */
|
|
190
190
|
REFRESH: `__${namespace}_refresh`,
|
|
191
191
|
/** JS-readable user data for client hydration (5 min) */
|
|
192
|
-
USER: `__${namespace}_user
|
|
192
|
+
USER: `__${namespace}_user`,
|
|
193
|
+
/** HttpOnly active organization ID used to preserve org-scoped sessions */
|
|
194
|
+
ACTIVE_ORG_ID: `__${namespace}_active_org_id`,
|
|
195
|
+
/** HttpOnly active organization slug used to preserve org-scoped sessions */
|
|
196
|
+
ACTIVE_ORG_SLUG: `__${namespace}_active_org_slug`
|
|
193
197
|
};
|
|
194
198
|
}
|
|
195
199
|
var SESSION_TOKEN_LIFETIME = SESSION_TOKEN_LIFETIME_SECONDS;
|
|
196
200
|
var REFRESH_TOKEN_LIFETIME = REFRESH_TOKEN_LIFETIME_SECONDS;
|
|
201
|
+
var ACTIVE_ORG_LIFETIME = REFRESH_TOKEN_LIFETIME_SECONDS;
|
|
197
202
|
var SECURE_COOKIE_OPTIONS = {
|
|
198
203
|
httpOnly: true,
|
|
199
204
|
secure: process.env.NODE_ENV === "production",
|
|
@@ -207,6 +212,10 @@ var USER_COOKIE_OPTIONS = {
|
|
|
207
212
|
sameSite: "lax",
|
|
208
213
|
path: "/"
|
|
209
214
|
};
|
|
215
|
+
var ACTIVE_ORG_COOKIE_OPTIONS = {
|
|
216
|
+
...SECURE_COOKIE_OPTIONS,
|
|
217
|
+
httpOnly: true
|
|
218
|
+
};
|
|
210
219
|
async function getAuthCookies(namespace) {
|
|
211
220
|
const cookieStore = await cookies();
|
|
212
221
|
const names = getCookieNames(namespace);
|
|
@@ -255,6 +264,8 @@ async function clearAuthCookies(namespace) {
|
|
|
255
264
|
cookieStore.delete(names.SESSION);
|
|
256
265
|
cookieStore.delete(names.REFRESH);
|
|
257
266
|
cookieStore.delete(names.USER);
|
|
267
|
+
cookieStore.delete(names.ACTIVE_ORG_ID);
|
|
268
|
+
cookieStore.delete(names.ACTIVE_ORG_SLUG);
|
|
258
269
|
}
|
|
259
270
|
async function isSessionExpired(namespace) {
|
|
260
271
|
const { expiresAt } = await getAuthCookies(namespace);
|
|
@@ -290,6 +301,8 @@ function clearAuthCookiesMiddleware(response, namespace) {
|
|
|
290
301
|
response.cookies.delete(names.SESSION);
|
|
291
302
|
response.cookies.delete(names.REFRESH);
|
|
292
303
|
response.cookies.delete(names.USER);
|
|
304
|
+
response.cookies.delete(names.ACTIVE_ORG_ID);
|
|
305
|
+
response.cookies.delete(names.ACTIVE_ORG_SLUG);
|
|
293
306
|
}
|
|
294
307
|
function parseUserCookie(value) {
|
|
295
308
|
try {
|
|
@@ -476,6 +489,18 @@ function matchesPattern(pathname, pattern) {
|
|
|
476
489
|
function matchesAny(pathname, patterns) {
|
|
477
490
|
return patterns.some((p) => matchesPattern(pathname, p));
|
|
478
491
|
}
|
|
492
|
+
function requestPathWithSearch(request) {
|
|
493
|
+
const { pathname, search } = request.nextUrl;
|
|
494
|
+
return `${pathname}${search}`;
|
|
495
|
+
}
|
|
496
|
+
function isSafeRelativeRedirectPath(value) {
|
|
497
|
+
return typeof value === "string" && value.startsWith("/") && !value.startsWith("//");
|
|
498
|
+
}
|
|
499
|
+
function resolveSafeRelativeRedirectPath(value, fallback) {
|
|
500
|
+
if (isSafeRelativeRedirectPath(value)) return value;
|
|
501
|
+
if (isSafeRelativeRedirectPath(fallback)) return fallback;
|
|
502
|
+
return "/";
|
|
503
|
+
}
|
|
479
504
|
async function handleCallback(request, ctx) {
|
|
480
505
|
const { searchParams } = request.nextUrl;
|
|
481
506
|
const code = searchParams.get("code");
|
|
@@ -569,6 +594,16 @@ function resolveOrgScopedTokenExpiresIn(data) {
|
|
|
569
594
|
if (typeof data.expires_in === "number") return data.expires_in;
|
|
570
595
|
return SESSION_TOKEN_LIFETIME;
|
|
571
596
|
}
|
|
597
|
+
function resolveOrgScopedAccessToken(data) {
|
|
598
|
+
if (data.accessToken) return data.accessToken;
|
|
599
|
+
if (data.access_token) return data.access_token;
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
function resolveOrgScopedRefreshToken(data, fallbackRefreshToken) {
|
|
603
|
+
if (data.refreshToken) return data.refreshToken;
|
|
604
|
+
if (data.refresh_token) return data.refresh_token;
|
|
605
|
+
return fallbackRefreshToken;
|
|
606
|
+
}
|
|
572
607
|
function resolveOrgScopedTokenType(data) {
|
|
573
608
|
if (data.tokenType) return data.tokenType;
|
|
574
609
|
if (data.token_type) return data.token_type;
|
|
@@ -577,11 +612,149 @@ function resolveOrgScopedTokenType(data) {
|
|
|
577
612
|
function parseUserCookie2(value) {
|
|
578
613
|
if (!value) return null;
|
|
579
614
|
try {
|
|
580
|
-
return JSON.parse(value);
|
|
615
|
+
return JSON.parse(decodeCookieValue(value));
|
|
581
616
|
} catch {
|
|
582
617
|
return null;
|
|
583
618
|
}
|
|
584
619
|
}
|
|
620
|
+
function decodeCookieValue(value) {
|
|
621
|
+
try {
|
|
622
|
+
return decodeURIComponent(value);
|
|
623
|
+
} catch {
|
|
624
|
+
return value;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function readFirstCookieValue(request, names) {
|
|
628
|
+
for (const name of names) {
|
|
629
|
+
const value = request.cookies.get(name)?.value;
|
|
630
|
+
if (value) return decodeCookieValue(value);
|
|
631
|
+
}
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
function uniqueCookieNames(names) {
|
|
635
|
+
return [...new Set(names.filter((name) => Boolean(name)))];
|
|
636
|
+
}
|
|
637
|
+
function readActiveOrganizationContext(request, ctx, sessionToken) {
|
|
638
|
+
const orgIdCookieNames = uniqueCookieNames([
|
|
639
|
+
ctx.cookieNames.ACTIVE_ORG_ID,
|
|
640
|
+
...ctx.config.organizationContext.additionalOrgIdCookies
|
|
641
|
+
]);
|
|
642
|
+
const orgSlugCookieNames = uniqueCookieNames([
|
|
643
|
+
ctx.cookieNames.ACTIVE_ORG_SLUG,
|
|
644
|
+
...ctx.config.organizationContext.additionalOrgSlugCookies
|
|
645
|
+
]);
|
|
646
|
+
const fromCookie = {
|
|
647
|
+
id: readFirstCookieValue(request, orgIdCookieNames),
|
|
648
|
+
slug: readFirstCookieValue(request, orgSlugCookieNames)
|
|
649
|
+
};
|
|
650
|
+
if (fromCookie.id || fromCookie.slug) return fromCookie;
|
|
651
|
+
const payload = sessionToken ? decodeJwtPayload(sessionToken) : null;
|
|
652
|
+
return {
|
|
653
|
+
id: payload?.org_id ?? null,
|
|
654
|
+
slug: payload?.org_slug ?? null
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function setActiveOrganizationCookies(response, ctx, org) {
|
|
658
|
+
if (org.id) {
|
|
659
|
+
response.cookies.set(ctx.cookieNames.ACTIVE_ORG_ID, org.id, {
|
|
660
|
+
...ACTIVE_ORG_COOKIE_OPTIONS,
|
|
661
|
+
maxAge: ACTIVE_ORG_LIFETIME
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
if (org.slug) {
|
|
665
|
+
response.cookies.set(ctx.cookieNames.ACTIVE_ORG_SLUG, org.slug, {
|
|
666
|
+
...ACTIVE_ORG_COOKIE_OPTIONS,
|
|
667
|
+
maxAge: ACTIVE_ORG_LIFETIME
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async function getUserOrganizations(accessToken, ctx) {
|
|
672
|
+
try {
|
|
673
|
+
const res = await fetch(`${ctx.platformUrl}/v1/orgs/memberships`, {
|
|
674
|
+
method: "GET",
|
|
675
|
+
headers: {
|
|
676
|
+
"Content-Type": "application/json",
|
|
677
|
+
"x-app-secret": ctx.secretKey,
|
|
678
|
+
Authorization: `Bearer ${accessToken}`
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
if (!res.ok) {
|
|
682
|
+
ctx.log("Organization memberships fetch failed", res.status);
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
const data = await res.json().catch(() => ({}));
|
|
686
|
+
return Array.isArray(data.organizations) ? data.organizations : null;
|
|
687
|
+
} catch (err) {
|
|
688
|
+
ctx.log("Organization memberships fetch error", err);
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async function getOrgScopedTokens(accessToken, orgId, ctx) {
|
|
693
|
+
try {
|
|
694
|
+
const res = await fetch(`${ctx.platformUrl}/v1/auth/switch-org`, {
|
|
695
|
+
method: "POST",
|
|
696
|
+
headers: {
|
|
697
|
+
"Content-Type": "application/json",
|
|
698
|
+
"x-app-secret": ctx.secretKey,
|
|
699
|
+
Authorization: `Bearer ${accessToken}`
|
|
700
|
+
},
|
|
701
|
+
body: JSON.stringify({ orgId })
|
|
702
|
+
});
|
|
703
|
+
const data = await res.json().catch(() => ({}));
|
|
704
|
+
if (!res.ok) {
|
|
705
|
+
ctx.log("Org scope restore failed", data.error || data.message || res.status);
|
|
706
|
+
return { status: res.status, data };
|
|
707
|
+
}
|
|
708
|
+
return { status: res.status, data };
|
|
709
|
+
} catch (err) {
|
|
710
|
+
ctx.log("Org scope restore error", err);
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
async function restoreOrganizationScopeAfterRefresh(request, ctx, refreshedTokens, previousSessionToken) {
|
|
715
|
+
if (!ctx.config.organizationContext.enabled) {
|
|
716
|
+
return { tokens: refreshedTokens, activeOrganization: null };
|
|
717
|
+
}
|
|
718
|
+
const activeOrg = readActiveOrganizationContext(request, ctx, previousSessionToken);
|
|
719
|
+
let targetOrgId = activeOrg.id;
|
|
720
|
+
let targetOrgSlug = activeOrg.slug;
|
|
721
|
+
let organizations = null;
|
|
722
|
+
if (!targetOrgId && targetOrgSlug) {
|
|
723
|
+
organizations = await getUserOrganizations(refreshedTokens.accessToken, ctx);
|
|
724
|
+
targetOrgId = organizations?.find((org) => org.slug === targetOrgSlug)?.id ?? null;
|
|
725
|
+
}
|
|
726
|
+
if (!targetOrgId && !targetOrgSlug && ctx.config.organizationContext.autoSelectSingleOrganization) {
|
|
727
|
+
organizations = organizations ?? await getUserOrganizations(refreshedTokens.accessToken, ctx);
|
|
728
|
+
const onlyOrg = organizations?.length === 1 ? organizations[0] : null;
|
|
729
|
+
targetOrgId = onlyOrg?.id ?? null;
|
|
730
|
+
targetOrgSlug = onlyOrg?.slug ?? null;
|
|
731
|
+
}
|
|
732
|
+
if (!targetOrgId) {
|
|
733
|
+
return { tokens: refreshedTokens, activeOrganization: null };
|
|
734
|
+
}
|
|
735
|
+
const scoped = await getOrgScopedTokens(refreshedTokens.accessToken, targetOrgId, ctx);
|
|
736
|
+
const scopedData = scoped?.data;
|
|
737
|
+
const accessToken = scopedData ? resolveOrgScopedAccessToken(scopedData) : null;
|
|
738
|
+
if (!scoped || scoped.status >= 400 || !scopedData || !accessToken) {
|
|
739
|
+
return { tokens: refreshedTokens, activeOrganization: null };
|
|
740
|
+
}
|
|
741
|
+
const payload = decodeJwtPayload(accessToken);
|
|
742
|
+
const resolvedOrg = {
|
|
743
|
+
id: payload?.org_id ?? targetOrgId,
|
|
744
|
+
slug: payload?.org_slug ?? targetOrgSlug
|
|
745
|
+
};
|
|
746
|
+
return {
|
|
747
|
+
tokens: {
|
|
748
|
+
...refreshedTokens,
|
|
749
|
+
accessToken,
|
|
750
|
+
refreshToken: resolveOrgScopedRefreshToken(scopedData, refreshedTokens.refreshToken),
|
|
751
|
+
expiresIn: resolveOrgScopedTokenExpiresIn(scopedData),
|
|
752
|
+
tokenType: resolveOrgScopedTokenType(scopedData),
|
|
753
|
+
user: scopedData.user ?? refreshedTokens.user
|
|
754
|
+
},
|
|
755
|
+
activeOrganization: resolvedOrg
|
|
756
|
+
};
|
|
757
|
+
}
|
|
585
758
|
async function handleSwitchOrg(request, ctx) {
|
|
586
759
|
ctx.log("Switch org request");
|
|
587
760
|
if (request.method !== "POST") {
|
|
@@ -592,9 +765,11 @@ async function handleSwitchOrg(request, ctx) {
|
|
|
592
765
|
return NextResponse.json({ error: "Not authenticated", accessToken: null }, { status: 401 });
|
|
593
766
|
}
|
|
594
767
|
let orgId = null;
|
|
768
|
+
let requestedOrgSlug = null;
|
|
595
769
|
try {
|
|
596
770
|
const body = await request.json();
|
|
597
771
|
orgId = typeof body.orgId === "string" && body.orgId.trim() ? body.orgId.trim() : null;
|
|
772
|
+
requestedOrgSlug = typeof body.orgSlug === "string" && body.orgSlug.trim() ? body.orgSlug.trim() : null;
|
|
598
773
|
} catch {
|
|
599
774
|
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
|
600
775
|
}
|
|
@@ -602,25 +777,20 @@ async function handleSwitchOrg(request, ctx) {
|
|
|
602
777
|
return NextResponse.json({ error: "orgId is required" }, { status: 400 });
|
|
603
778
|
}
|
|
604
779
|
try {
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
"Content-Type": "application/json",
|
|
609
|
-
"x-app-secret": ctx.secretKey,
|
|
610
|
-
Authorization: `Bearer ${sessionToken}`
|
|
611
|
-
},
|
|
612
|
-
body: JSON.stringify({ orgId })
|
|
613
|
-
});
|
|
614
|
-
const data = await res.json().catch(() => ({}));
|
|
615
|
-
if (!res.ok) {
|
|
780
|
+
const scoped = await getOrgScopedTokens(sessionToken, orgId, ctx);
|
|
781
|
+
const data = scoped?.data;
|
|
782
|
+
if (!scoped || scoped.status >= 400 || !data) {
|
|
616
783
|
return NextResponse.json(
|
|
617
|
-
{ error: data
|
|
618
|
-
{ status:
|
|
784
|
+
{ error: data?.error || data?.message || "switch_org_failed" },
|
|
785
|
+
{ status: scoped?.status ?? 502 }
|
|
619
786
|
);
|
|
620
787
|
}
|
|
621
|
-
const accessToken = data
|
|
788
|
+
const accessToken = resolveOrgScopedAccessToken(data);
|
|
622
789
|
if (!accessToken) {
|
|
623
|
-
return NextResponse.json(
|
|
790
|
+
return NextResponse.json(
|
|
791
|
+
{ error: data.error || data.message || "invalid_response" },
|
|
792
|
+
{ status: 502 }
|
|
793
|
+
);
|
|
624
794
|
}
|
|
625
795
|
const expiresIn = resolveOrgScopedTokenExpiresIn(data);
|
|
626
796
|
const expiresAt = Date.now() + expiresIn * 1e3;
|
|
@@ -650,6 +820,11 @@ async function handleSwitchOrg(request, ctx) {
|
|
|
650
820
|
}
|
|
651
821
|
);
|
|
652
822
|
}
|
|
823
|
+
const payload = decodeJwtPayload(accessToken);
|
|
824
|
+
setActiveOrganizationCookies(response, ctx, {
|
|
825
|
+
id: payload?.org_id ?? orgId,
|
|
826
|
+
slug: payload?.org_slug ?? requestedOrgSlug
|
|
827
|
+
});
|
|
653
828
|
ctx.log("Switch org success", { orgId });
|
|
654
829
|
return response;
|
|
655
830
|
} catch (err) {
|
|
@@ -724,6 +899,12 @@ function createSylphxMiddleware(userConfig = {}) {
|
|
|
724
899
|
debug: userConfig.debug ?? false,
|
|
725
900
|
orgRequired: userConfig.orgRequired ?? false,
|
|
726
901
|
orgSelectionUrl: userConfig.orgSelectionUrl ?? "/select-organization",
|
|
902
|
+
organizationContext: {
|
|
903
|
+
enabled: userConfig.organizationContext?.enabled ?? true,
|
|
904
|
+
autoSelectSingleOrganization: userConfig.organizationContext?.autoSelectSingleOrganization ?? true,
|
|
905
|
+
additionalOrgIdCookies: userConfig.organizationContext?.additionalOrgIdCookies ?? [],
|
|
906
|
+
additionalOrgSlugCookies: userConfig.organizationContext?.additionalOrgSlugCookies ?? []
|
|
907
|
+
},
|
|
727
908
|
onResponse: userConfig.onResponse
|
|
728
909
|
};
|
|
729
910
|
const publicRoutes = [
|
|
@@ -778,16 +959,25 @@ function createSylphxMiddleware(userConfig = {}) {
|
|
|
778
959
|
const sessionToken = request.cookies.get(cookieNames.SESSION)?.value;
|
|
779
960
|
const refreshToken = request.cookies.get(cookieNames.REFRESH)?.value;
|
|
780
961
|
const hasValidSession = sessionToken && !isTokenExpired(sessionToken);
|
|
781
|
-
const response = NextResponse.next();
|
|
962
|
+
const response = NextResponse.next({ request: { headers: request.headers } });
|
|
782
963
|
let isAuthenticated = hasValidSession;
|
|
783
964
|
let activeSessionToken = hasValidSession ? sessionToken : null;
|
|
784
965
|
if (!hasValidSession && refreshToken) {
|
|
785
966
|
log("Session expired, refreshing");
|
|
786
967
|
const tokens = await refreshTokens(refreshToken, ctx);
|
|
787
968
|
if (tokens) {
|
|
788
|
-
|
|
969
|
+
const restored = await restoreOrganizationScopeAfterRefresh(
|
|
970
|
+
request,
|
|
971
|
+
ctx,
|
|
972
|
+
tokens,
|
|
973
|
+
sessionToken
|
|
974
|
+
);
|
|
975
|
+
setAuthCookiesMiddleware(response, namespace, restored.tokens);
|
|
976
|
+
if (restored.activeOrganization) {
|
|
977
|
+
setActiveOrganizationCookies(response, ctx, restored.activeOrganization);
|
|
978
|
+
}
|
|
789
979
|
isAuthenticated = true;
|
|
790
|
-
activeSessionToken = tokens.accessToken;
|
|
980
|
+
activeSessionToken = restored.tokens.accessToken;
|
|
791
981
|
} else {
|
|
792
982
|
clearAuthCookiesMiddleware(response, namespace);
|
|
793
983
|
isAuthenticated = false;
|
|
@@ -798,19 +988,23 @@ function createSylphxMiddleware(userConfig = {}) {
|
|
|
798
988
|
if (!isPublic && !isAuthenticated) {
|
|
799
989
|
log("Redirecting to sign-in");
|
|
800
990
|
const url = new URL(config.signInUrl, request.url);
|
|
801
|
-
url.searchParams.set("
|
|
991
|
+
url.searchParams.set("callbackUrl", requestPathWithSearch(request));
|
|
802
992
|
return NextResponse.redirect(url);
|
|
803
993
|
}
|
|
804
994
|
if (isAuthenticated && pathname === config.signInUrl) {
|
|
805
|
-
|
|
806
|
-
|
|
995
|
+
const callbackUrl = resolveSafeRelativeRedirectPath(
|
|
996
|
+
request.nextUrl.searchParams.get("callbackUrl"),
|
|
997
|
+
config.afterSignInUrl
|
|
998
|
+
);
|
|
999
|
+
log("Redirecting from sign-in", { callbackUrl });
|
|
1000
|
+
return NextResponse.redirect(new URL(callbackUrl, request.url));
|
|
807
1001
|
}
|
|
808
1002
|
if (config.orgRequired && !isPublic && isAuthenticated && activeSessionToken && !matchesPattern(pathname, config.orgSelectionUrl)) {
|
|
809
1003
|
const payload = decodeJwtPayload(activeSessionToken);
|
|
810
1004
|
if (!payload?.org_id) {
|
|
811
1005
|
log("Redirecting to organization selection");
|
|
812
1006
|
const url = new URL(config.orgSelectionUrl, request.url);
|
|
813
|
-
url.searchParams.set("redirect_to",
|
|
1007
|
+
url.searchParams.set("redirect_to", requestPathWithSearch(request));
|
|
814
1008
|
return NextResponse.redirect(url);
|
|
815
1009
|
}
|
|
816
1010
|
}
|
|
@@ -2263,6 +2457,8 @@ async function getSessionToken() {
|
|
|
2263
2457
|
return sessionToken;
|
|
2264
2458
|
}
|
|
2265
2459
|
export {
|
|
2460
|
+
ACTIVE_ORG_COOKIE_OPTIONS,
|
|
2461
|
+
ACTIVE_ORG_LIFETIME,
|
|
2266
2462
|
REFRESH_TOKEN_LIFETIME,
|
|
2267
2463
|
SECURE_COOKIE_OPTIONS,
|
|
2268
2464
|
SESSION_TOKEN_LIFETIME,
|