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 +1 -1
- package/templates/src/components/compositor/Compositor.tsx +6 -0
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +7 -2
- package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +3 -0
- package/templates/src/pages/api/sandbox.ts +40 -5
- package/templates/src/pages/sandbox.astro +57 -60
- package/templates/src/stores/storykeep.ts +1 -12
package/package.json
CHANGED
|
@@ -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: {
|
|
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
|
|
21
|
-
|
|
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: '
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
<div class="pointer-events-auto flex-shrink-0">
|
|
136
|
+
<StoryKeepToolBar client:only="react" />
|
|
137
|
+
</div>
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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);
|