strapi-plugin-magic-sessionmanager 3.0.2 → 3.2.0

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 CHANGED
@@ -101,11 +101,33 @@ npm run build
101
101
  npm run develop
102
102
  ```
103
103
 
104
- ### 4. Access Admin Dashboard
104
+ ### 4. Configure Encryption (Important!) 🔐
105
+
106
+ Generate a secure encryption key for JWT token storage:
107
+
108
+ ```bash
109
+ # Option 1: Use Admin Panel
110
+ # Go to Admin → Sessions → Settings → Security Settings
111
+ # Click "Generate Key" and copy to .env
112
+
113
+ # Option 2: Generate manually
114
+ node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
115
+
116
+ # Add to .env file:
117
+ SESSION_ENCRYPTION_KEY=your-generated-32-char-key-here
118
+ ```
119
+
120
+ **Why this is important:**
121
+ - JWT tokens are encrypted before storing in database
122
+ - Prevents token exposure if database is compromised
123
+ - Uses AES-256-GCM encryption standard
124
+
125
+ ### 5. Access Admin Dashboard
105
126
 
106
127
  - Navigate to Strapi Admin: `http://localhost:1337/admin`
107
128
  - Find **Sessions** in the left sidebar under plugins
108
129
  - Start with the **License** tab to activate your license
130
+ - Go to **Settings → Security** to generate your encryption key
109
131
 
110
132
  ---
111
133
 
@@ -699,6 +721,104 @@ Available through Admin UI **Settings → Sessions → Settings**:
699
721
 
700
722
  ---
701
723
 
724
+ ## 🔐 JWT Token Security
725
+
726
+ ### Encryption
727
+
728
+ All JWT tokens are **encrypted before storing** in the database using **AES-256-GCM** encryption.
729
+
730
+ #### Why Encrypt Tokens?
731
+
732
+ ```
733
+ ❌ Without Encryption:
734
+ Database compromised → Attacker sees JWTs → Can impersonate users!
735
+
736
+ ✅ With Encryption:
737
+ Database compromised → Attacker sees encrypted data → Useless without key!
738
+ ```
739
+
740
+ #### How It Works
741
+
742
+ ```
743
+ Login: User gets JWT
744
+
745
+ JWT: "eyJhbGciOiJIUzI1NiIs..."
746
+
747
+ [Encrypt with AES-256-GCM]
748
+
749
+ Encrypted: "a3f7b2c1:8c4d9e2a:f2a5b8c3d4e5f6a7..."
750
+
751
+ Stored in Database (secure!)
752
+
753
+ Logout: User sends JWT
754
+
755
+ [Fetch all active sessions from DB]
756
+
757
+ [Decrypt each token]
758
+
759
+ [Compare with user's JWT]
760
+
761
+ Match found → Terminate session ✅
762
+ ```
763
+
764
+ #### Configuration
765
+
766
+ **Generate Encryption Key (Admin Panel):**
767
+
768
+ 1. Go to **Admin → Sessions → Settings**
769
+ 2. Open **Security Settings** accordion
770
+ 3. Find **JWT Encryption Key Generator**
771
+ 4. Click **"Generate Key"**
772
+ 5. Copy key with **"Copy for .env"** button
773
+ 6. Add to your `.env` file
774
+
775
+ **Or generate manually:**
776
+
777
+ ```bash
778
+ # Generate secure 32-byte key
779
+ node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
780
+
781
+ # Add to .env
782
+ SESSION_ENCRYPTION_KEY=aBc123XyZ...your-32-char-key
783
+ ```
784
+
785
+ **Fallback Behavior:**
786
+
787
+ If `SESSION_ENCRYPTION_KEY` is not set:
788
+ - Plugin uses `APP_KEYS` or `API_TOKEN_SALT` as fallback
789
+ - ⚠️ Warning logged on startup
790
+ - Still encrypted, but key is derived from Strapi's keys
791
+
792
+ **Production Recommendation:**
793
+ Always use a dedicated `SESSION_ENCRYPTION_KEY` for better security isolation.
794
+
795
+ #### Security Details
796
+
797
+ | Feature | Value |
798
+ |---------|-------|
799
+ | Algorithm | AES-256-GCM |
800
+ | Key Size | 256 bits (32 bytes) |
801
+ | IV Length | 128 bits (16 bytes) |
802
+ | Auth Tag | 128 bits (16 bytes) |
803
+ | Format | `iv:authTag:encryptedData` (hex) |
804
+
805
+ ### Unique Session IDs
806
+
807
+ Each session gets a cryptographically unique identifier:
808
+
809
+ ```javascript
810
+ sessionId: "sess_lx3k7_4f2a8b3c_a1b2c3d4e5f6"
811
+ // prefix^ ^timestamp ^user-hash ^random-bytes
812
+ ```
813
+
814
+ **Benefits:**
815
+ - ✅ No collisions across sessions
816
+ - ✅ Traceable session identifiers
817
+ - ✅ Independent from database IDs
818
+ - ✅ URL-safe for future features
819
+
820
+ ---
821
+
702
822
  ## 🔒 Premium Features
703
823
 
704
824
  ### IP Geolocation & Threat Detection
@@ -366,6 +366,20 @@ Login Details:
366
366
  },
367
367
  });
368
368
 
369
+ // ================ HELPER FUNCTIONS ================
370
+ const generateSecureKey = () => {
371
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
372
+ let key = '';
373
+ const array = new Uint8Array(32);
374
+ crypto.getRandomValues(array);
375
+
376
+ for (let i = 0; i < 32; i++) {
377
+ key += chars[array[i] % chars.length];
378
+ }
379
+
380
+ return key;
381
+ };
382
+
369
383
  const SettingsPage = () => {
370
384
  const { get, post, put } = useFetchClient();
371
385
  const { toggleNotification } = useNotification();
@@ -375,6 +389,8 @@ const SettingsPage = () => {
375
389
  const [hasChanges, setHasChanges] = useState(false);
376
390
  const [cleaning, setCleaning] = useState(false);
377
391
  const [activeTemplateTab, setActiveTemplateTab] = useState('suspiciousLogin');
392
+ const [encryptionKey, setEncryptionKey] = useState('');
393
+ const [showEncryptionKey, setShowEncryptionKey] = useState(false);
378
394
 
379
395
  const [settings, setSettings] = useState({
380
396
  inactivityTimeout: 15,
@@ -772,6 +788,136 @@ const SettingsPage = () => {
772
788
  <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
773
789
  🔒 SECURITY OPTIONS
774
790
  </Typography>
791
+
792
+ {/* Encryption Key Generator */}
793
+ <Box
794
+ background="neutral0"
795
+ padding={6}
796
+ style={{
797
+ borderRadius: theme.borderRadius.lg,
798
+ marginBottom: '32px',
799
+ border: `2px solid ${theme.colors.primary[100]}`,
800
+ background: `linear-gradient(135deg, ${theme.colors.neutral[0]} 0%, ${theme.colors.primary[50]} 100%)`
801
+ }}
802
+ >
803
+ <Flex direction="column" gap={4}>
804
+ <Flex alignItems="center" gap={3}>
805
+ <Shield style={{ width: 24, height: 24, color: theme.colors.primary[600] }} />
806
+ <Typography variant="delta" fontWeight="bold">
807
+ JWT Encryption Key Generator
808
+ </Typography>
809
+ </Flex>
810
+
811
+ <Typography variant="omega" textColor="neutral600" style={{ lineHeight: 1.6 }}>
812
+ Generate a secure 32-character encryption key for JWT token storage.
813
+ This key is used to encrypt tokens before saving them to the database.
814
+ </Typography>
815
+
816
+ <Alert
817
+ variant="default"
818
+ title="Important"
819
+ style={{ marginTop: 8 }}
820
+ >
821
+ Add this key to your <code>.env</code> file as <strong>SESSION_ENCRYPTION_KEY</strong> for production.
822
+ </Alert>
823
+
824
+ <Flex gap={3} alignItems="flex-end">
825
+ <Box style={{ flex: 1 }}>
826
+ <TextInput
827
+ label="Generated Encryption Key"
828
+ value={encryptionKey}
829
+ onChange={(e) => setEncryptionKey(e.target.value)}
830
+ placeholder="Click 'Generate Key' to create a secure key"
831
+ type={showEncryptionKey ? 'text' : 'password'}
832
+ />
833
+ </Box>
834
+ <Button
835
+ variant="secondary"
836
+ onClick={() => setShowEncryptionKey(!showEncryptionKey)}
837
+ size="L"
838
+ >
839
+ {showEncryptionKey ? 'Hide' : 'Show'}
840
+ </Button>
841
+ </Flex>
842
+
843
+ <Flex gap={3}>
844
+ <Button
845
+ variant="default"
846
+ startIcon={<Code />}
847
+ onClick={() => {
848
+ const key = generateSecureKey();
849
+ setEncryptionKey(key);
850
+ setShowEncryptionKey(true);
851
+ toggleNotification({
852
+ type: 'success',
853
+ message: '32-character encryption key generated!'
854
+ });
855
+ }}
856
+ size="L"
857
+ >
858
+ Generate Key
859
+ </Button>
860
+
861
+ <Button
862
+ variant="tertiary"
863
+ startIcon={<Duplicate />}
864
+ onClick={() => {
865
+ if (encryptionKey) {
866
+ navigator.clipboard.writeText(encryptionKey);
867
+ toggleNotification({
868
+ type: 'success',
869
+ message: 'Encryption key copied to clipboard!'
870
+ });
871
+ }
872
+ }}
873
+ disabled={!encryptionKey}
874
+ size="L"
875
+ >
876
+ Copy to Clipboard
877
+ </Button>
878
+
879
+ <Button
880
+ variant="tertiary"
881
+ startIcon={<Duplicate />}
882
+ onClick={() => {
883
+ if (encryptionKey) {
884
+ const envLine = `SESSION_ENCRYPTION_KEY=${encryptionKey}`;
885
+ navigator.clipboard.writeText(envLine);
886
+ toggleNotification({
887
+ type: 'success',
888
+ message: 'Copied as .env format!'
889
+ });
890
+ }
891
+ }}
892
+ disabled={!encryptionKey}
893
+ size="L"
894
+ >
895
+ Copy for .env
896
+ </Button>
897
+ </Flex>
898
+
899
+ {encryptionKey && (
900
+ <Box
901
+ padding={4}
902
+ background="neutral100"
903
+ style={{
904
+ borderRadius: theme.borderRadius.md,
905
+ border: '1px solid ' + theme.colors.neutral[200],
906
+ fontFamily: 'monospace',
907
+ fontSize: '12px',
908
+ wordBreak: 'break-all'
909
+ }}
910
+ >
911
+ <Typography variant="omega" fontWeight="bold" style={{ marginBottom: 8, display: 'block' }}>
912
+ Add to .env file:
913
+ </Typography>
914
+ <code style={{ color: theme.colors.primary[700] }}>
915
+ SESSION_ENCRYPTION_KEY={encryptionKey}
916
+ </code>
917
+ </Box>
918
+ )}
919
+ </Flex>
920
+ </Box>
775
921
 
776
922
  {/* Feature Toggles */}
777
923
  <Box background="neutral100" padding={5} style={{ borderRadius: theme.borderRadius.md, marginBottom: '32px' }}>
@@ -4,8 +4,8 @@ import { useFetchClient } from "@strapi/strapi/admin";
4
4
  import styled, { css, keyframes } from "styled-components";
5
5
  import { Loader, Typography, Box, Flex, Badge } from "@strapi/design-system";
6
6
  import { ChartBubble, Crown, User, Clock, Monitor } from "@strapi/icons";
7
- import { a as pluginId } from "./index--JzOiQNw.mjs";
8
- import { u as useLicense } from "./useLicense-W1cxUaca.mjs";
7
+ import { a as pluginId } from "./index-B-0VPfeF.mjs";
8
+ import { u as useLicense } from "./useLicense-DUGjNbQ9.mjs";
9
9
  const theme = {
10
10
  colors: {
11
11
  primary: { 100: "#E0F2FE", 500: "#0EA5E9", 600: "#0284C7" },
@@ -6,8 +6,8 @@ const admin = require("@strapi/strapi/admin");
6
6
  const styled = require("styled-components");
7
7
  const designSystem = require("@strapi/design-system");
8
8
  const icons = require("@strapi/icons");
9
- const index = require("./index-DqtQaEBL.js");
10
- const useLicense = require("./useLicense-DA-averf.js");
9
+ const index = require("./index-W_QbTAYU.js");
10
+ const useLicense = require("./useLicense-C_Rneohy.js");
11
11
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
12
12
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
13
13
  const theme = {
@@ -4,8 +4,8 @@ import { useFetchClient, useNotification } from "@strapi/strapi/admin";
4
4
  import styled, { css, keyframes } from "styled-components";
5
5
  import { Modal, Flex, Box, Typography, Badge, Divider, Button, Loader, SingleSelect, SingleSelectOption, Thead, Tr, Th, Tbody, Td, Table, TextInput } from "@strapi/design-system";
6
6
  import { Check, Information, Monitor, Server, Clock, Cross, Earth, Shield, Crown, Phone, Download, User, Eye, Trash, Search, Key } from "@strapi/icons";
7
- import { p as parseUserAgent, a as pluginId } from "./index--JzOiQNw.mjs";
8
- import { u as useLicense } from "./useLicense-W1cxUaca.mjs";
7
+ import { p as parseUserAgent, a as pluginId } from "./index-B-0VPfeF.mjs";
8
+ import { u as useLicense } from "./useLicense-DUGjNbQ9.mjs";
9
9
  import { useNavigate } from "react-router-dom";
10
10
  const TwoColumnGrid = styled.div`
11
11
  display: grid;
@@ -6,8 +6,8 @@ const admin = require("@strapi/strapi/admin");
6
6
  const styled = require("styled-components");
7
7
  const designSystem = require("@strapi/design-system");
8
8
  const icons = require("@strapi/icons");
9
- const index = require("./index-DqtQaEBL.js");
10
- const useLicense = require("./useLicense-DA-averf.js");
9
+ const index = require("./index-W_QbTAYU.js");
10
+ const useLicense = require("./useLicense-C_Rneohy.js");
11
11
  const reactRouterDom = require("react-router-dom");
12
12
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
13
13
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
@@ -4,7 +4,7 @@ import { Loader, Box, Alert, Flex, Typography, Button, Badge, Accordion } from "
4
4
  import { useFetchClient, useNotification } from "@strapi/strapi/admin";
5
5
  import { ArrowClockwise, Duplicate, Download, User, Shield, Sparkle, ChartBubble } from "@strapi/icons";
6
6
  import styled, { css, keyframes } from "styled-components";
7
- import { a as pluginId } from "./index--JzOiQNw.mjs";
7
+ import { a as pluginId } from "./index-B-0VPfeF.mjs";
8
8
  const theme = {
9
9
  colors: {
10
10
  neutral: { 200: "#E5E7EB" }
@@ -6,7 +6,7 @@ const designSystem = require("@strapi/design-system");
6
6
  const admin = require("@strapi/strapi/admin");
7
7
  const icons = require("@strapi/icons");
8
8
  const styled = require("styled-components");
9
- const index = require("./index-DqtQaEBL.js");
9
+ const index = require("./index-W_QbTAYU.js");
10
10
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
11
11
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
12
12
  const theme = {
@@ -1,17 +1,17 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from "react";
3
- import { Flex, Loader, Typography, Button, Box, Badge, Accordion, Grid, SingleSelect, SingleSelectOption, Divider, Toggle, NumberInput, Checkbox, Tabs, TextInput } from "@strapi/design-system";
3
+ import { Flex, Loader, Typography, Button, Box, Badge, Accordion, Grid, SingleSelect, SingleSelectOption, Divider, Alert, TextInput, Toggle, NumberInput, Checkbox, Tabs } from "@strapi/design-system";
4
4
  import { useFetchClient, useNotification } from "@strapi/strapi/admin";
5
- import { Check, Information, Cog, Trash, Shield, Mail, Code } from "@strapi/icons";
5
+ import { Check, Information, Cog, Trash, Shield, Code, Duplicate, Mail } from "@strapi/icons";
6
6
  import styled, { css, keyframes } from "styled-components";
7
- import { a as pluginId } from "./index--JzOiQNw.mjs";
8
- import { u as useLicense } from "./useLicense-W1cxUaca.mjs";
7
+ import { a as pluginId } from "./index-B-0VPfeF.mjs";
8
+ import { u as useLicense } from "./useLicense-DUGjNbQ9.mjs";
9
9
  const theme = {
10
10
  colors: {
11
- primary: { 600: "#0284C7", 700: "#075985" },
11
+ primary: { 600: "#0284C7", 700: "#075985", 100: "#E0F2FE", 50: "#F0F9FF" },
12
12
  success: { 600: "#16A34A", 700: "#15803D" },
13
13
  danger: { 600: "#DC2626" },
14
- neutral: { 200: "#E5E7EB", 400: "#9CA3AF", 700: "#374151" }
14
+ neutral: { 0: "#FFFFFF", 200: "#E5E7EB", 400: "#9CA3AF", 700: "#374151" }
15
15
  },
16
16
  shadows: { sm: "0 1px 3px rgba(0,0,0,0.1)" },
17
17
  borderRadius: { md: "8px", lg: "12px" }
@@ -322,6 +322,16 @@ Login Details:
322
322
  - VPN: {{reason.isVpn}}, Proxy: {{reason.isProxy}}`
323
323
  }
324
324
  });
325
+ const generateSecureKey = () => {
326
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?";
327
+ let key = "";
328
+ const array = new Uint8Array(32);
329
+ crypto.getRandomValues(array);
330
+ for (let i = 0; i < 32; i++) {
331
+ key += chars[array[i] % chars.length];
332
+ }
333
+ return key;
334
+ };
325
335
  const SettingsPage = () => {
326
336
  const { get, post, put } = useFetchClient();
327
337
  const { toggleNotification } = useNotification();
@@ -331,6 +341,8 @@ const SettingsPage = () => {
331
341
  const [hasChanges, setHasChanges] = useState(false);
332
342
  const [cleaning, setCleaning] = useState(false);
333
343
  const [activeTemplateTab, setActiveTemplateTab] = useState("suspiciousLogin");
344
+ const [encryptionKey, setEncryptionKey] = useState("");
345
+ const [showEncryptionKey, setShowEncryptionKey] = useState(false);
334
346
  const [settings, setSettings] = useState({
335
347
  inactivityTimeout: 15,
336
348
  cleanupInterval: 30,
@@ -658,6 +670,142 @@ const SettingsPage = () => {
658
670
  ) }),
659
671
  /* @__PURE__ */ jsx(Accordion.Content, { children: /* @__PURE__ */ jsxs(Box, { padding: 6, children: [
660
672
  /* @__PURE__ */ jsx(Typography, { variant: "sigma", fontWeight: "bold", style: { marginBottom: "16px", display: "block", color: theme.colors.neutral[700] }, children: "🔒 SECURITY OPTIONS" }),
673
+ /* @__PURE__ */ jsx(
674
+ Box,
675
+ {
676
+ background: "neutral0",
677
+ padding: 6,
678
+ style: {
679
+ borderRadius: theme.borderRadius.lg,
680
+ marginBottom: "32px",
681
+ border: `2px solid ${theme.colors.primary[100]}`,
682
+ background: `linear-gradient(135deg, ${theme.colors.neutral[0]} 0%, ${theme.colors.primary[50]} 100%)`
683
+ },
684
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
685
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 3, children: [
686
+ /* @__PURE__ */ jsx(Shield, { style: { width: 24, height: 24, color: theme.colors.primary[600] } }),
687
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", children: "JWT Encryption Key Generator" })
688
+ ] }),
689
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral600", style: { lineHeight: 1.6 }, children: "Generate a secure 32-character encryption key for JWT token storage. This key is used to encrypt tokens before saving them to the database." }),
690
+ /* @__PURE__ */ jsxs(
691
+ Alert,
692
+ {
693
+ variant: "default",
694
+ title: "Important",
695
+ style: { marginTop: 8 },
696
+ children: [
697
+ "Add this key to your ",
698
+ /* @__PURE__ */ jsx("code", { children: ".env" }),
699
+ " file as ",
700
+ /* @__PURE__ */ jsx("strong", { children: "SESSION_ENCRYPTION_KEY" }),
701
+ " for production."
702
+ ]
703
+ }
704
+ ),
705
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-end", children: [
706
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
707
+ TextInput,
708
+ {
709
+ label: "Generated Encryption Key",
710
+ value: encryptionKey,
711
+ onChange: (e) => setEncryptionKey(e.target.value),
712
+ placeholder: "Click 'Generate Key' to create a secure key",
713
+ type: showEncryptionKey ? "text" : "password"
714
+ }
715
+ ) }),
716
+ /* @__PURE__ */ jsx(
717
+ Button,
718
+ {
719
+ variant: "secondary",
720
+ onClick: () => setShowEncryptionKey(!showEncryptionKey),
721
+ size: "L",
722
+ children: showEncryptionKey ? "Hide" : "Show"
723
+ }
724
+ )
725
+ ] }),
726
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, children: [
727
+ /* @__PURE__ */ jsx(
728
+ Button,
729
+ {
730
+ variant: "default",
731
+ startIcon: /* @__PURE__ */ jsx(Code, {}),
732
+ onClick: () => {
733
+ const key = generateSecureKey();
734
+ setEncryptionKey(key);
735
+ setShowEncryptionKey(true);
736
+ toggleNotification({
737
+ type: "success",
738
+ message: "32-character encryption key generated!"
739
+ });
740
+ },
741
+ size: "L",
742
+ children: "Generate Key"
743
+ }
744
+ ),
745
+ /* @__PURE__ */ jsx(
746
+ Button,
747
+ {
748
+ variant: "tertiary",
749
+ startIcon: /* @__PURE__ */ jsx(Duplicate, {}),
750
+ onClick: () => {
751
+ if (encryptionKey) {
752
+ navigator.clipboard.writeText(encryptionKey);
753
+ toggleNotification({
754
+ type: "success",
755
+ message: "Encryption key copied to clipboard!"
756
+ });
757
+ }
758
+ },
759
+ disabled: !encryptionKey,
760
+ size: "L",
761
+ children: "Copy to Clipboard"
762
+ }
763
+ ),
764
+ /* @__PURE__ */ jsx(
765
+ Button,
766
+ {
767
+ variant: "tertiary",
768
+ startIcon: /* @__PURE__ */ jsx(Duplicate, {}),
769
+ onClick: () => {
770
+ if (encryptionKey) {
771
+ const envLine = `SESSION_ENCRYPTION_KEY=${encryptionKey}`;
772
+ navigator.clipboard.writeText(envLine);
773
+ toggleNotification({
774
+ type: "success",
775
+ message: "Copied as .env format!"
776
+ });
777
+ }
778
+ },
779
+ disabled: !encryptionKey,
780
+ size: "L",
781
+ children: "Copy for .env"
782
+ }
783
+ )
784
+ ] }),
785
+ encryptionKey && /* @__PURE__ */ jsxs(
786
+ Box,
787
+ {
788
+ padding: 4,
789
+ background: "neutral100",
790
+ style: {
791
+ borderRadius: theme.borderRadius.md,
792
+ border: "1px solid " + theme.colors.neutral[200],
793
+ fontFamily: "monospace",
794
+ fontSize: "12px",
795
+ wordBreak: "break-all"
796
+ },
797
+ children: [
798
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "bold", style: { marginBottom: 8, display: "block" }, children: "Add to .env file:" }),
799
+ /* @__PURE__ */ jsxs("code", { style: { color: theme.colors.primary[700] }, children: [
800
+ "SESSION_ENCRYPTION_KEY=",
801
+ encryptionKey
802
+ ] })
803
+ ]
804
+ }
805
+ )
806
+ ] })
807
+ }
808
+ ),
661
809
  /* @__PURE__ */ jsx(Box, { background: "neutral100", padding: 5, style: { borderRadius: theme.borderRadius.md, marginBottom: "32px" }, children: /* @__PURE__ */ jsxs(Grid.Root, { gap: 4, children: [
662
810
  /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsx(
663
811
  ToggleCard,