astro-tractstack 2.1.0 → 2.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Astro integration for TractStack - the digital experience platform (DXP) for the missing middle",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,6 +18,7 @@ import {
18
18
  brandConfigStore,
19
19
  viewportModeStore,
20
20
  setViewportMode,
21
+ sandboxTokenStore,
21
22
  } from '@/stores/storykeep';
22
23
  import { getCtx, ROOT_NODE_NAME, type NodesContext } from '@/stores/nodes';
23
24
  import { stopLoadingAnimation } from '@/utils/helpers';
@@ -53,6 +54,7 @@ export type CompositorProps = {
53
54
  urlParams: Record<string, string | boolean>;
54
55
  fullCanonicalURL: string;
55
56
  isSandboxMode?: boolean;
57
+ sandboxToken?: string;
56
58
  };
57
59
 
58
60
  const VERBOSE = false;
@@ -300,12 +302,16 @@ export const Compositor = (props: CompositorProps) => {
300
302
  preferredThemeStore.set(props.config.THEME as Theme);
301
303
  codehookMapStore.set(props.availableCodeHooks);
302
304
  brandConfigStore.set(props.config);
305
+ if (props.sandboxToken) {
306
+ sandboxTokenStore.set(props.sandboxToken);
307
+ }
303
308
  }, [
304
309
  props.fullContentMap,
305
310
  props.urlParams,
306
311
  props.fullCanonicalURL,
307
312
  props.availableCodeHooks,
308
313
  props.config,
314
+ props.sandboxToken,
309
315
  ]);
310
316
 
311
317
  // Initialize nodes tree and set up subscriptions
@@ -7,7 +7,7 @@ import SquaresPlusIcon from '@heroicons/react/24/outline/SquaresPlusIcon';
7
7
  import DocumentIcon from '@heroicons/react/24/outline/DocumentIcon';
8
8
  import { NodesContext, getCtx } from '@/stores/nodes';
9
9
  import { cloneDeep } from '@/utils/helpers';
10
- import { hasAssemblyAIStore } from '@/stores/storykeep';
10
+ import { hasAssemblyAIStore, sandboxTokenStore } from '@/stores/storykeep';
11
11
  import prompts from '@/constants/prompts.json';
12
12
  import type { DesignLibraryEntry } from '@/types/tractstack';
13
13
  import { PaneAddMode, type TemplatePane } from '@/types/compositorTypes';
@@ -60,9 +60,14 @@ const callAskLemurAPI = async (
60
60
  let resultData: any;
61
61
 
62
62
  if (isSandboxMode) {
63
+ const token = sandboxTokenStore.get();
63
64
  const response = await fetch(`/api/sandbox`, {
64
65
  method: 'POST',
65
- headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ 'X-Tenant-ID': tenantId,
69
+ 'X-Sandbox-Token': token || '',
70
+ },
66
71
  credentials: 'include',
67
72
  body: JSON.stringify({ action: 'askLemur', payload: requestBody }),
68
73
  });
@@ -6,6 +6,7 @@ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
6
6
  import SparklesIcon from '@heroicons/react/24/outline/SparklesIcon';
7
7
  import { getCtx } from '@/stores/nodes';
8
8
  import { selectionStore } from '@/stores/selection';
9
+ import { sandboxTokenStore } from '@/stores/storykeep';
9
10
  import { AiDesignStep, type AiDesignConfig } from './steps/AiDesignStep';
10
11
  import prompts from '@/constants/prompts.json';
11
12
  import { TractStackAPI } from '@/utils/api';
@@ -34,11 +35,13 @@ const callAskLemurAPI = async (
34
35
  let resultData: any;
35
36
 
36
37
  if (isSandboxMode) {
38
+ const token = sandboxTokenStore.get();
37
39
  const response = await fetch(`/api/sandbox`, {
38
40
  method: 'POST',
39
41
  headers: {
40
42
  'Content-Type': 'application/json',
41
43
  'X-Tenant-ID': tenantId,
44
+ 'X-Sandbox-Token': token || '',
42
45
  },
43
46
  credentials: 'include',
44
47
  body: JSON.stringify({ action: 'askLemur', payload: requestBody }),
@@ -1,3 +1,4 @@
1
+ import { createHmac } from 'node:crypto';
1
2
  import type { APIRoute } from '@/types/astro';
2
3
 
3
4
  export const POST: APIRoute = async ({ request, locals }) => {
@@ -17,14 +18,48 @@ export const POST: APIRoute = async ({ request, locals }) => {
17
18
  );
18
19
  }
19
20
 
20
- const profileCookie = request.headers
21
- .get('cookie')
22
- ?.includes('tractstack_profile');
23
- if (!profileCookie) {
21
+ const tokenHeader = request.headers.get('X-Sandbox-Token');
22
+ if (!tokenHeader) {
24
23
  return new Response(
25
24
  JSON.stringify({
26
25
  success: false,
27
- error: 'Forbidden: Missing sandbox profile.',
26
+ error: 'Unauthorized: Missing sandbox token',
27
+ }),
28
+ { status: 403, headers: { 'Content-Type': 'application/json' } }
29
+ );
30
+ }
31
+
32
+ const [timestamp, signature] = tokenHeader.split('.');
33
+ if (!timestamp || !signature) {
34
+ return new Response(
35
+ JSON.stringify({
36
+ success: false,
37
+ error: 'Unauthorized: Invalid token format',
38
+ }),
39
+ { status: 403, headers: { 'Content-Type': 'application/json' } }
40
+ );
41
+ }
42
+
43
+ const now = Date.now();
44
+ if (now - parseInt(timestamp, 10) > 2 * 60 * 60 * 1000) {
45
+ return new Response(
46
+ JSON.stringify({
47
+ success: false,
48
+ error: 'Session expired. Please refresh the page.',
49
+ }),
50
+ { status: 403, headers: { 'Content-Type': 'application/json' } }
51
+ );
52
+ }
53
+
54
+ const expectedSignature = createHmac('sha256', sharedSecret)
55
+ .update(timestamp)
56
+ .digest('hex');
57
+
58
+ if (signature !== expectedSignature) {
59
+ return new Response(
60
+ JSON.stringify({
61
+ success: false,
62
+ error: 'Unauthorized: Invalid signature',
28
63
  }),
29
64
  { status: 403, headers: { 'Content-Type': 'application/json' } }
30
65
  );
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import { ulid } from 'ulid';
3
+ import { createHmac } from 'node:crypto';
3
4
  import Layout from '@/layouts/Layout.astro';
4
5
  import Header from '@/components/Header.astro';
5
6
  import { getFullContentMap } from '@/stores/analytics';
@@ -49,9 +50,11 @@ for (const [key, value] of Astro.url.searchParams) {
49
50
  urlParams[key] = value === '' ? true : value;
50
51
  }
51
52
 
52
- const hasProfile = Astro.request.headers
53
- .get('cookie')
54
- ?.includes('tractstack_profile=true');
53
+ const timestamp = Date.now().toString();
54
+ const signature = createHmac('sha256', import.meta.env.PRIVATE_SANDBOX_SECRET)
55
+ .update(timestamp)
56
+ .digest('hex');
57
+ const sandboxToken = `${timestamp}.${signature}`;
55
58
  ---
56
59
 
57
60
  <Layout
@@ -81,71 +84,65 @@ const hasProfile = Astro.request.headers
81
84
  menu={null}
82
85
  />
83
86
 
84
- {
85
- hasProfile && (
86
- <>
87
- <section
88
- id="storykeepHeader"
89
- role="banner"
90
- class="left-0 right-0 z-101 bg-mywhite drop-shadow transition-all duration-200"
87
+ <section
88
+ id="storykeepHeader"
89
+ role="banner"
90
+ class="left-0 right-0 z-101 bg-mywhite drop-shadow transition-all duration-200"
91
+ >
92
+ <StoryKeepHeader
93
+ slug="sandbox"
94
+ isContext={false}
95
+ isSandboxMode={true}
96
+ client:only="react"
97
+ />
98
+ </section>
99
+
100
+ <div class="flex min-h-screen">
101
+ <StoryKeepToolMode isContext={false} client:only="react" />
102
+
103
+ <main id="mainContent" class="relative flex-1 overflow-x-auto">
104
+ <div class="h-full bg-myblue/20 bg-mylightgrey p-1.5">
105
+ <div
106
+ class="h-fit min-h-screen pb-96"
107
+ style={{
108
+ backgroundImage:
109
+ 'repeating-linear-gradient(135deg, transparent, transparent 10px, rgba(0,0,0,0.05) 10px, rgba(0,0,0,0.05) 20px)',
110
+ }}
91
111
  >
92
- <StoryKeepHeader
93
- slug="sandbox"
94
- isContext={false}
112
+ <Compositor
113
+ id={storyFragmentID}
114
+ nodes={loadData}
115
+ config={brandConfig}
116
+ fullContentMap={fullContentMap}
117
+ fullCanonicalURL="/sandbox"
118
+ urlParams={urlParams}
119
+ availableCodeHooks={Object.keys(codeHookComponents)}
95
120
  isSandboxMode={true}
121
+ sandboxToken={sandboxToken}
96
122
  client:only="react"
97
123
  />
98
- </section>
99
-
100
- <div class="flex min-h-screen">
101
- <StoryKeepToolMode isContext={false} client:only="react" />
102
-
103
- <main id="mainContent" class="relative flex-1 overflow-x-auto">
104
- <div class="h-full bg-myblue/20 bg-mylightgrey p-1.5">
105
- <div
106
- class="h-fit min-h-screen pb-96"
107
- style={{
108
- backgroundImage:
109
- 'repeating-linear-gradient(135deg, transparent, transparent 10px, rgba(0,0,0,0.05) 10px, rgba(0,0,0,0.05) 20px)',
110
- }}
111
- >
112
- <Compositor
113
- id={storyFragmentID}
114
- nodes={loadData}
115
- config={brandConfig}
116
- fullContentMap={fullContentMap}
117
- fullCanonicalURL="/sandbox"
118
- urlParams={urlParams}
119
- availableCodeHooks={Object.keys(codeHookComponents)}
120
- isSandboxMode={true}
121
- client:only="react"
122
- />
123
- </div>
124
- </div>
125
- </main>
126
124
  </div>
125
+ </div>
126
+ </main>
127
+ </div>
127
128
 
128
- <aside
129
- id="settingsControls"
130
- class="pointer-events-none fixed bottom-16 right-2 z-101 flex flex-col items-end gap-2 md:bottom-2"
131
- >
132
- <div class="pointer-events-none flex-grow" />
129
+ <aside
130
+ id="settingsControls"
131
+ class="pointer-events-none fixed bottom-16 right-2 z-101 flex flex-col items-end gap-2 md:bottom-2"
132
+ >
133
+ <div class="pointer-events-none flex-grow"></div>
133
134
 
134
- <div class="pointer-events-auto flex-shrink-0">
135
- <StoryKeepToolBar client:only="react" />
136
- </div>
135
+ <div class="pointer-events-auto flex-shrink-0">
136
+ <StoryKeepToolBar client:only="react" />
137
+ </div>
137
138
 
138
- <div class="pointer-events-auto max-h-full">
139
- <SettingsPanel
140
- config={brandConfig}
141
- availableCodeHooks={Object.keys(codeHookComponents)}
142
- client:only="react"
143
- />
144
- </div>
145
- </aside>
146
- </>
147
- )
148
- }
139
+ <div class="pointer-events-auto max-h-full">
140
+ <SettingsPanel
141
+ availableCodeHooks={Object.keys(codeHookComponents)}
142
+ client:only="react"
143
+ />
144
+ </div>
145
+ </aside>
149
146
  </Layout>
150
147
 
151
148
  <script>
@@ -30,8 +30,8 @@ export const codehookMapStore = atom<string[]>([]);
30
30
  export const pendingHomePageSlugStore = atom<string | null>(null);
31
31
 
32
32
  export const saasModalOpenStore = atom<boolean>(false);
33
+ export const sandboxTokenStore = atom<string | null>(null);
33
34
 
34
- // Tool mode types
35
35
  export type ToolModeVal =
36
36
  | 'styles'
37
37
  | 'text'
@@ -40,7 +40,6 @@ export type ToolModeVal =
40
40
  | 'move'
41
41
  | 'debug';
42
42
 
43
- // Tool add mode types
44
43
  export type ToolAddMode =
45
44
  | 'p'
46
45
  | 'h2'
@@ -54,10 +53,8 @@ export type ToolAddMode =
54
53
  | 'identify'
55
54
  | 'toggle';
56
55
 
57
- // Header positioning state
58
56
  export type HeaderPositionState = 'normal' | 'sticky';
59
57
 
60
- // Viewport and display state
61
58
  export const viewportModeStore = atom<ViewportKey>('auto');
62
59
  export const viewportKeyStore = map<{
63
60
  value: 'mobile' | 'tablet' | 'desktop';
@@ -69,26 +66,19 @@ export const isEditingStore = atom<boolean>(false);
69
66
 
70
67
  export const showAnalyticsStore = atom<boolean>(false);
71
68
 
72
- // Header positioning
73
69
  export const headerPositionStore = atom<HeaderPositionState>('normal');
74
70
 
75
- // Settings panel state
76
71
  export const settingsPanelOpenStore = atom<boolean>(false);
77
72
  export const addPanelOpenStore = atom<boolean>(false);
78
73
 
79
- // Mobile-specific behavior
80
74
  export const mobileHeaderFadedStore = atom<boolean>(false);
81
75
 
82
- // Undo/redo state
83
76
  export const canUndoStore = atom<boolean>(false);
84
77
  export const canRedoStore = atom<boolean>(false);
85
78
 
86
- // Actions
87
79
  export const toggleSettingsPanel = () => {
88
80
  const isOpen = !settingsPanelOpenStore.get();
89
81
  settingsPanelOpenStore.set(isOpen);
90
-
91
- // Handle mobile behavior
92
82
  handleSettingsPanelMobile(isOpen);
93
83
  };
94
84
  export const toggleAddPanel = () => {
@@ -112,7 +102,6 @@ const getViewportFromWidth = (
112
102
 
113
103
  export const setViewportMode = (mode: ViewportKey) => {
114
104
  viewportModeStore.set(mode);
115
- // Sync viewportKeyStore
116
105
  if (mode === 'auto') {
117
106
  const actualViewport = getViewportFromWidth(window.innerWidth);
118
107
  viewportKeyStore.setKey('value', actualViewport);