@sylphx/sdk 0.10.2 → 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.
@@ -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 };
@@ -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 res = await fetch(`${ctx.platformUrl}/v1/auth/switch-org`, {
606
- method: "POST",
607
- headers: {
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.error || data.message || "switch_org_failed" },
618
- { status: res.status }
784
+ { error: data?.error || data?.message || "switch_org_failed" },
785
+ { status: scoped?.status ?? 502 }
619
786
  );
620
787
  }
621
- const accessToken = data.accessToken ?? data.access_token;
788
+ const accessToken = resolveOrgScopedAccessToken(data);
622
789
  if (!accessToken) {
623
- return NextResponse.json({ error: "invalid_response" }, { status: 502 });
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
- setAuthCookiesMiddleware(response, namespace, tokens);
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("redirect_to", pathname);
991
+ url.searchParams.set("callbackUrl", requestPathWithSearch(request));
802
992
  return NextResponse.redirect(url);
803
993
  }
804
994
  if (isAuthenticated && pathname === config.signInUrl) {
805
- log("Redirecting from sign-in to dashboard");
806
- return NextResponse.redirect(new URL(config.afterSignInUrl, request.url));
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", pathname);
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,