astro-tractstack 2.1.1 → 2.1.3

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.3",
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",
@@ -10,6 +10,7 @@ import type { ResourceNode } from '@/types/compositorTypes';
10
10
 
11
11
  export interface Props {
12
12
  target: string;
13
+ paneId: string;
13
14
  fullContentMap: FullContentMapItem[];
14
15
  resourcesPayload?: Record<string, ResourceNode[]>;
15
16
  options?: {
@@ -13,6 +13,7 @@ import type { ResourceNode } from '@/types/compositorTypes';
13
13
 
14
14
  export interface Props {
15
15
  target: string;
16
+ paneId: string;
16
17
  fullContentMap: FullContentMapItem[];
17
18
  resourcesPayload?: Record<string, ResourceNode[]>;
18
19
  options?: {
@@ -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 }),
@@ -158,6 +158,7 @@ paneIds.forEach((paneId: string) => {
158
158
  <div class="relative overflow-hidden">
159
159
  <CodeHook
160
160
  target={codeHookTargets[paneId]}
161
+ paneId={paneId}
161
162
  options={(() => {
162
163
  const optionsStr =
163
164
  codeHookTargets[paneId + '-' + codeHookTargets[paneId]];
@@ -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
  );
@@ -126,6 +126,7 @@ if (!brandConfig.SITE_INIT) {
126
126
  codeHookTarget ? (
127
127
  <CodeHook
128
128
  target={codeHookTarget}
129
+ paneId={paneId}
129
130
  resourcesPayload={resourcesPayload}
130
131
  fullContentMap={fullContentMap}
131
132
  />
@@ -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);