astro-tractstack 2.1.1 → 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.1",
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';
@@ -48,6 +49,12 @@ const urlParams: Record<string, string | boolean> = {};
48
49
  for (const [key, value] of Astro.url.searchParams) {
49
50
  urlParams[key] = value === '' ? true : value;
50
51
  }
52
+
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}`;
51
58
  ---
52
59
 
53
60
  <Layout
@@ -111,6 +118,7 @@ for (const [key, value] of Astro.url.searchParams) {
111
118
  urlParams={urlParams}
112
119
  availableCodeHooks={Object.keys(codeHookComponents)}
113
120
  isSandboxMode={true}
121
+ sandboxToken={sandboxToken}
114
122
  client:only="react"
115
123
  />
116
124
  </div>
@@ -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);