astro-tractstack 2.0.38 → 2.0.40
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/dist/index.js +14 -10
- package/package.json +1 -1
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +33 -27
- package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +12 -25
- package/templates/src/layouts/Layout.astro +12 -2
- package/templates/src/middleware.ts +17 -76
- package/templates/src/utils/api.ts +11 -36
- package/templates/src/utils/tenantResolver.ts +113 -0
- package/utils/inject-files.ts +4 -0
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ function b(t) {
|
|
|
10
10
|
}
|
|
11
11
|
function g(t, e) {
|
|
12
12
|
e.info("TractStack configuration applied"), t.enableMultiTenant && e.info("Multi-tenant mode enabled"), t.includeExamples && e.info("Example components will be included");
|
|
13
|
-
const c = process.env.PUBLIC_GO_BACKEND,
|
|
13
|
+
const c = process.env.PUBLIC_GO_BACKEND, r = process.env.PUBLIC_TENANTID;
|
|
14
14
|
if (!c)
|
|
15
15
|
e.warn("PUBLIC_GO_BACKEND not set - this will be required at runtime");
|
|
16
16
|
else
|
|
@@ -19,11 +19,11 @@ function g(t, e) {
|
|
|
19
19
|
} catch {
|
|
20
20
|
e.error(`PUBLIC_GO_BACKEND is not a valid URL: ${c}`);
|
|
21
21
|
}
|
|
22
|
-
return
|
|
22
|
+
return r ? /^[a-zA-Z0-9_-]+$/.test(r) ? e.info(`Tenant ID validated: ${r}`) : e.error(`PUBLIC_TENANTID contains invalid characters: ${r}`) : e.warn("PUBLIC_TENANTID not set - this will be required at runtime"), t;
|
|
23
23
|
}
|
|
24
24
|
async function w(t, e, c) {
|
|
25
25
|
e.info("TractStack: Injecting template files");
|
|
26
|
-
const
|
|
26
|
+
const r = [
|
|
27
27
|
// Core Configuration
|
|
28
28
|
{
|
|
29
29
|
src: t("../templates/env.example"),
|
|
@@ -751,6 +751,10 @@ async function w(t, e, c) {
|
|
|
751
751
|
src: t("../templates/src/utils/api.ts"),
|
|
752
752
|
dest: "src/utils/api.ts"
|
|
753
753
|
},
|
|
754
|
+
{
|
|
755
|
+
src: t("../templates/src/utils/tenantResolver.ts"),
|
|
756
|
+
dest: "src/utils/tenantResolver.ts"
|
|
757
|
+
},
|
|
754
758
|
{
|
|
755
759
|
src: t("../templates/src/utils/api/brandConfig.ts"),
|
|
756
760
|
dest: "src/utils/api/brandConfig.ts"
|
|
@@ -2144,12 +2148,12 @@ async function w(t, e, c) {
|
|
|
2144
2148
|
}
|
|
2145
2149
|
] : []
|
|
2146
2150
|
];
|
|
2147
|
-
for (const s of
|
|
2151
|
+
for (const s of r)
|
|
2148
2152
|
try {
|
|
2149
2153
|
const p = i(s.dest);
|
|
2150
2154
|
n(p) || x(p, { recursive: !0 });
|
|
2151
|
-
const
|
|
2152
|
-
if (!n(s.dest) ||
|
|
2155
|
+
const o = !s.protected && (s.dest === "tailwind.config.cjs" || s.dest.startsWith("src/components/codehooks/") || s.dest.startsWith("src/components/widgets/") || s.dest.startsWith("src/") || s.dest.startsWith("public/client/") || s.dest === ".gitignore");
|
|
2156
|
+
if (!n(s.dest) || o)
|
|
2153
2157
|
if (n(s.src))
|
|
2154
2158
|
k(s.src, s.dest), e.info(`Updated ${s.dest}`);
|
|
2155
2159
|
else {
|
|
@@ -2158,8 +2162,8 @@ async function w(t, e, c) {
|
|
|
2158
2162
|
}
|
|
2159
2163
|
else s.protected ? e.info(`Protected: ${s.dest} (skipped overwrite)`) : e.info(`Skipped existing ${s.dest}`);
|
|
2160
2164
|
} catch (p) {
|
|
2161
|
-
const
|
|
2162
|
-
e.error(`Failed to create ${s.dest}: ${
|
|
2165
|
+
const o = p instanceof Error ? p.message : String(p);
|
|
2166
|
+
e.error(`Failed to create ${s.dest}: ${o}`);
|
|
2163
2167
|
}
|
|
2164
2168
|
}
|
|
2165
2169
|
function _(t) {
|
|
@@ -2178,7 +2182,7 @@ function C(t = {}) {
|
|
|
2178
2182
|
return {
|
|
2179
2183
|
name: "astro-tractstack",
|
|
2180
2184
|
hooks: {
|
|
2181
|
-
"astro:config:setup": async ({ config: c, updateConfig:
|
|
2185
|
+
"astro:config:setup": async ({ config: c, updateConfig: r, logger: s }) => {
|
|
2182
2186
|
g(t, s);
|
|
2183
2187
|
const p = t.enableMultiTenant || !1;
|
|
2184
2188
|
if (s.info(
|
|
@@ -2198,7 +2202,7 @@ function C(t = {}) {
|
|
|
2198
2202
|
), new Error(
|
|
2199
2203
|
"TractStack requires an SSR adapter. Please add @astrojs/node adapter to your astro.config.mjs"
|
|
2200
2204
|
);
|
|
2201
|
-
|
|
2205
|
+
r({
|
|
2202
2206
|
vite: {
|
|
2203
2207
|
define: {
|
|
2204
2208
|
__TRACTSTACK_VERSION__: JSON.stringify("2.0.0-alpha.1"),
|
package/package.json
CHANGED
|
@@ -24,6 +24,7 @@ import { DirectInjectStep } from './steps/DirectInjectStep';
|
|
|
24
24
|
import BooleanToggle from '@/components/form/BooleanToggle';
|
|
25
25
|
import EnumSelect from '@/components/form/EnumSelect';
|
|
26
26
|
import type { StoryFragmentNode } from '@/types/compositorTypes';
|
|
27
|
+
import { TractStackAPI } from '@/utils/api'; // <--- IMPORT ADDED
|
|
27
28
|
|
|
28
29
|
type Step =
|
|
29
30
|
| 'initial'
|
|
@@ -51,9 +52,10 @@ const callAskLemurAPI = async (
|
|
|
51
52
|
expectJson: boolean,
|
|
52
53
|
isSandboxMode: boolean
|
|
53
54
|
): Promise<string> => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const tenantId =
|
|
55
|
+
// FIX: Use the centralized API class to ensure correct Tenant ID resolution
|
|
56
|
+
const api = new TractStackAPI();
|
|
57
|
+
const tenantId = api.getTenantId(); // Gets correct ID from window config
|
|
58
|
+
|
|
57
59
|
const requestBody = {
|
|
58
60
|
prompt,
|
|
59
61
|
input_text: context,
|
|
@@ -62,43 +64,47 @@ const callAskLemurAPI = async (
|
|
|
62
64
|
max_tokens: 2000,
|
|
63
65
|
};
|
|
64
66
|
|
|
65
|
-
let
|
|
67
|
+
let resultData: any;
|
|
68
|
+
|
|
66
69
|
if (isSandboxMode) {
|
|
67
|
-
|
|
70
|
+
// Sandbox mode still uses local fetch, but we pass the correct tenant ID
|
|
71
|
+
const response = await fetch(`/api/sandbox`, {
|
|
68
72
|
method: 'POST',
|
|
69
73
|
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
|
|
70
74
|
credentials: 'include',
|
|
71
75
|
body: JSON.stringify({ action: 'askLemur', payload: requestBody }),
|
|
72
76
|
});
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const errorText = await response.text();
|
|
80
|
+
throw new Error(`Sandbox API failed: ${response.status} ${errorText}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const json = await response.json();
|
|
84
|
+
if (!json.success) {
|
|
85
|
+
throw new Error(json.error || 'Sandbox generation failed');
|
|
86
|
+
}
|
|
87
|
+
resultData = json.data; // { response: ... }
|
|
73
88
|
} else {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
|
|
77
|
-
credentials: 'include',
|
|
78
|
-
body: JSON.stringify(requestBody),
|
|
79
|
-
});
|
|
80
|
-
}
|
|
89
|
+
// Production mode: Use the robust API class
|
|
90
|
+
const response = await api.post('/api/v1/aai/askLemur', requestBody);
|
|
81
91
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const errorJson = JSON.parse(errorText);
|
|
87
|
-
if (errorJson?.error) backendError = errorJson.error;
|
|
88
|
-
} catch (e) {
|
|
89
|
-
/* ignore */
|
|
92
|
+
if (!response.success) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
response.error || 'Generation failed to return valid response.'
|
|
95
|
+
);
|
|
90
96
|
}
|
|
91
|
-
|
|
97
|
+
|
|
98
|
+
// TractStackAPI unwraps the 'data' field automatically
|
|
99
|
+
resultData = response.data; // { response: ... }
|
|
92
100
|
}
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
throw new Error(
|
|
97
|
-
result.error || 'Generation failed to return valid response.'
|
|
98
|
-
);
|
|
102
|
+
if (!resultData?.response) {
|
|
103
|
+
throw new Error('Generation failed to return a response object.');
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
let rawResponseData =
|
|
106
|
+
let rawResponseData = resultData.response;
|
|
107
|
+
|
|
102
108
|
if (expectJson && typeof rawResponseData === 'object') {
|
|
103
109
|
return JSON.stringify(rawResponseData);
|
|
104
110
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '@/components/compositor/preview/PaneSnapshotGenerator';
|
|
11
11
|
import { PaneAddMode, type StoryFragmentNode } from '@/types/compositorTypes';
|
|
12
12
|
import type { FullContentMapItem } from '@/types/tractstack';
|
|
13
|
+
import { TractStackAPI } from '@/utils/api';
|
|
13
14
|
|
|
14
15
|
interface AddPaneReUsePanelProps {
|
|
15
16
|
nodeId: string;
|
|
@@ -127,23 +128,15 @@ const AddPaneReUsePanel = ({
|
|
|
127
128
|
const fetchFragments = async () => {
|
|
128
129
|
try {
|
|
129
130
|
const paneIds = visiblePreviews.map((preview) => preview.pane.id);
|
|
131
|
+
const api = new TractStackAPI();
|
|
130
132
|
|
|
131
|
-
const
|
|
132
|
-
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
133
|
-
const response = await fetch(`${goBackend}/api/v1/fragments/panes`, {
|
|
134
|
-
method: 'POST',
|
|
135
|
-
headers: {
|
|
136
|
-
'Content-Type': 'application/json',
|
|
137
|
-
'X-Tenant-ID': import.meta.env.PUBLIC_TENANTID || 'default',
|
|
138
|
-
},
|
|
139
|
-
body: JSON.stringify({ paneIds }),
|
|
140
|
-
});
|
|
133
|
+
const response = await api.post('/api/v1/fragments/panes', { paneIds });
|
|
141
134
|
|
|
142
|
-
if (!response.
|
|
143
|
-
throw new Error(`Fragment API failed
|
|
135
|
+
if (!response.success) {
|
|
136
|
+
throw new Error(response.error || `Fragment API failed`);
|
|
144
137
|
}
|
|
145
138
|
|
|
146
|
-
const data =
|
|
139
|
+
const data = response.data;
|
|
147
140
|
|
|
148
141
|
setPreviews((prevPreviews) => {
|
|
149
142
|
const updated = [...prevPreviews];
|
|
@@ -206,22 +199,16 @@ const AddPaneReUsePanel = ({
|
|
|
206
199
|
if (!selectedPaneId) return;
|
|
207
200
|
|
|
208
201
|
try {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
`${goBackend}/api/v1/nodes/panes/${selectedPaneId}/template`,
|
|
213
|
-
{
|
|
214
|
-
headers: {
|
|
215
|
-
'X-Tenant-ID': import.meta.env.PUBLIC_TENANTID || 'default',
|
|
216
|
-
},
|
|
217
|
-
}
|
|
202
|
+
const api = new TractStackAPI();
|
|
203
|
+
const response = await api.get(
|
|
204
|
+
`/api/v1/nodes/panes/${selectedPaneId}/template`
|
|
218
205
|
);
|
|
219
206
|
|
|
220
|
-
if (!response.
|
|
221
|
-
throw new Error(`Template API failed
|
|
207
|
+
if (!response.success) {
|
|
208
|
+
throw new Error(response.error || `Template API failed`);
|
|
222
209
|
}
|
|
223
210
|
|
|
224
|
-
const templateData =
|
|
211
|
+
const templateData = response.data;
|
|
225
212
|
const ctx = getCtx();
|
|
226
213
|
|
|
227
214
|
// Find storyfragment
|
|
@@ -7,6 +7,7 @@ import { getBrandConfig } from '@/utils/api/brandConfig';
|
|
|
7
7
|
import { freshInstallStore } from '@/stores/backend';
|
|
8
8
|
import type { MenuNode } from '@/types/tractstack';
|
|
9
9
|
import type { ImpressionNode } from '@/types/compositorTypes';
|
|
10
|
+
import { resolveTenantId } from '@/utils/tenantResolver';
|
|
10
11
|
export interface Props {
|
|
11
12
|
title: string;
|
|
12
13
|
slug?: string;
|
|
@@ -49,8 +50,17 @@ const {
|
|
|
49
50
|
|
|
50
51
|
const isInitialized = !freshInstallStore.get().needsSetup;
|
|
51
52
|
const goBackend = import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
// Resolve tenant explicitly "Above the Fences" to guarantee the client gets the truth
|
|
54
|
+
const resolution = await resolveTenantId(Astro.request);
|
|
55
|
+
const tenantId = resolution.id;
|
|
56
|
+
if (!Astro.locals.tenant) {
|
|
57
|
+
Astro.locals.tenant = {
|
|
58
|
+
id: tenantId,
|
|
59
|
+
domain: Astro.url.hostname,
|
|
60
|
+
isMultiTenant: true,
|
|
61
|
+
isLocalhost: false,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
54
64
|
const brandConfig = propBrandConfig || (await getBrandConfig(tenantId));
|
|
55
65
|
const cssBasePath = isInitialized ? '/media/css' : '/styles';
|
|
56
66
|
const fontBasePath = isInitialized ? '/media/fonts' : '/fonts';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { APIContext, MiddlewareNext } from '@/types/astro';
|
|
2
|
+
import { resolveTenantId } from '@/utils/tenantResolver';
|
|
2
3
|
|
|
3
4
|
interface Locals {
|
|
4
5
|
tenant?: {
|
|
@@ -9,14 +10,6 @@ interface Locals {
|
|
|
9
10
|
};
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
interface CacheEntry {
|
|
13
|
-
tenantId: string;
|
|
14
|
-
timestamp: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const domainCache = new Map<string, CacheEntry>();
|
|
18
|
-
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
19
|
-
|
|
20
13
|
export async function onRequest(
|
|
21
14
|
context: APIContext & { locals: Locals },
|
|
22
15
|
next: MiddlewareNext
|
|
@@ -25,74 +18,22 @@ export async function onRequest(
|
|
|
25
18
|
import.meta.env.PUBLIC_ENABLE_MULTI_TENANT === 'true';
|
|
26
19
|
|
|
27
20
|
if (isMultiTenantEnabled && !import.meta.env.DEV) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
parts.length >= 4 &&
|
|
45
|
-
parts[1] === 'sandbox' &&
|
|
46
|
-
['freewebpress', 'tractstack'].includes(parts[2]) &&
|
|
47
|
-
parts[3] === 'com'
|
|
48
|
-
) {
|
|
49
|
-
tenantId = parts[0];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Strategy 2: Cache Lookup (Fast)
|
|
53
|
-
if (!tenantId) {
|
|
54
|
-
const cached = domainCache.get(hostname);
|
|
55
|
-
if (cached) {
|
|
56
|
-
if (Date.now() - cached.timestamp < CACHE_TTL) {
|
|
57
|
-
tenantId = cached.tenantId;
|
|
58
|
-
} else {
|
|
59
|
-
domainCache.delete(hostname);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Strategy 3: Backend Lookup (Fallback)
|
|
65
|
-
if (!tenantId) {
|
|
66
|
-
try {
|
|
67
|
-
// We assume the backend is always reachable on localhost:8080 in this architecture
|
|
68
|
-
const response = await fetch(
|
|
69
|
-
`http://127.0.0.1:8080/api/v1/resolve-domain?host=${encodeURIComponent(hostname)}`
|
|
70
|
-
);
|
|
71
|
-
if (response.ok) {
|
|
72
|
-
const data = await response.json();
|
|
73
|
-
if (data.tenantId) {
|
|
74
|
-
tenantId = data.tenantId;
|
|
75
|
-
domainCache.set(hostname, {
|
|
76
|
-
tenantId: data.tenantId,
|
|
77
|
-
timestamp: Date.now(),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
} catch (error) {
|
|
82
|
-
console.error(`Failed to resolve domain ${hostname}:`, error);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (tenantId) {
|
|
87
|
-
context.locals.tenant = {
|
|
88
|
-
id: tenantId,
|
|
89
|
-
domain: hostname,
|
|
90
|
-
isMultiTenant: true,
|
|
91
|
-
isLocalhost: false,
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
context.request.headers.set('X-Tenant-ID', tenantId);
|
|
95
|
-
}
|
|
21
|
+
// Use the shared utility to resolve the tenant ID
|
|
22
|
+
const resolution = await resolveTenantId(context.request);
|
|
23
|
+
|
|
24
|
+
if (resolution.id && resolution.id !== 'default') {
|
|
25
|
+
const hostname =
|
|
26
|
+
context.request.headers.get('x-forwarded-host') ||
|
|
27
|
+
context.request.headers.get('host');
|
|
28
|
+
|
|
29
|
+
context.locals.tenant = {
|
|
30
|
+
id: resolution.id,
|
|
31
|
+
domain: hostname,
|
|
32
|
+
isMultiTenant: true,
|
|
33
|
+
isLocalhost: false,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
context.request.headers.set('X-Tenant-ID', resolution.id);
|
|
96
37
|
}
|
|
97
38
|
}
|
|
98
39
|
|
|
@@ -20,7 +20,6 @@ export interface TractStackEvent {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function getConfig() {
|
|
23
|
-
// Server-side safety check
|
|
24
23
|
if (typeof window === 'undefined') {
|
|
25
24
|
return {
|
|
26
25
|
goBackend: import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080',
|
|
@@ -40,47 +39,26 @@ function getConfig() {
|
|
|
40
39
|
};
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
//function getTenantFromDomain(): string {
|
|
44
|
-
// if (typeof window === 'undefined') return 'default';
|
|
45
|
-
//
|
|
46
|
-
// const hostname = window.location.hostname;
|
|
47
|
-
//
|
|
48
|
-
// //if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
|
49
|
-
// // return 'default';
|
|
50
|
-
// //}
|
|
51
|
-
//
|
|
52
|
-
// const parts = hostname.split('.');
|
|
53
|
-
// if (
|
|
54
|
-
// parts.length >= 4 &&
|
|
55
|
-
// parts[1] === 'sandbox' &&
|
|
56
|
-
// ['tractstack', 'freewebpress'].includes(parts[2]) &&
|
|
57
|
-
// parts[3] === 'com'
|
|
58
|
-
// ) {
|
|
59
|
-
// return parts[0];
|
|
60
|
-
// }
|
|
61
|
-
//
|
|
62
|
-
// return 'default';
|
|
63
|
-
//}
|
|
64
|
-
|
|
65
42
|
export class TractStackAPI {
|
|
66
|
-
private
|
|
67
|
-
private tenantId: string;
|
|
43
|
+
private explicitTenantId?: string;
|
|
68
44
|
|
|
69
45
|
constructor(tenantId?: string) {
|
|
70
|
-
|
|
71
|
-
this.baseUrl = config.goBackend;
|
|
72
|
-
this.tenantId = tenantId || config.tenantId;
|
|
46
|
+
this.explicitTenantId = tenantId;
|
|
73
47
|
}
|
|
74
48
|
|
|
75
49
|
async request<T = any>(
|
|
76
50
|
endpoint: string,
|
|
77
51
|
options: RequestInit = {}
|
|
78
52
|
): Promise<APIResponse<T>> {
|
|
79
|
-
const
|
|
53
|
+
const config = getConfig();
|
|
54
|
+
const effectiveTenantId = this.explicitTenantId || config.tenantId;
|
|
55
|
+
const baseUrl = config.goBackend;
|
|
56
|
+
|
|
57
|
+
const url = `${baseUrl}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
|
|
80
58
|
|
|
81
59
|
const defaultHeaders = {
|
|
82
60
|
'Content-Type': 'application/json',
|
|
83
|
-
'X-Tenant-ID':
|
|
61
|
+
'X-Tenant-ID': effectiveTenantId,
|
|
84
62
|
...(typeof window !== 'undefined' &&
|
|
85
63
|
(window as any).TRACTSTACK_CONFIG?.sessionId && {
|
|
86
64
|
'X-TractStack-Session-ID': (window as any).TRACTSTACK_CONFIG
|
|
@@ -170,11 +148,12 @@ export class TractStackAPI {
|
|
|
170
148
|
}
|
|
171
149
|
|
|
172
150
|
getTenantId(): string {
|
|
173
|
-
|
|
151
|
+
const config = getConfig();
|
|
152
|
+
return this.explicitTenantId || config.tenantId;
|
|
174
153
|
}
|
|
175
154
|
|
|
176
155
|
setTenantId(tenantId: string): void {
|
|
177
|
-
this.
|
|
156
|
+
this.explicitTenantId = tenantId;
|
|
178
157
|
}
|
|
179
158
|
|
|
180
159
|
async getContentMapWithTimestamp(
|
|
@@ -185,11 +164,7 @@ export class TractStackAPI {
|
|
|
185
164
|
endpoint += `?lastUpdated=${lastUpdated}`;
|
|
186
165
|
}
|
|
187
166
|
|
|
188
|
-
// Use the raw request method to get the full response
|
|
189
167
|
const response = await this.request(endpoint);
|
|
190
|
-
|
|
191
|
-
// For this endpoint, the backend returns {data: [...], lastUpdated: 123} directly
|
|
192
|
-
// So response.data IS the {data: [...], lastUpdated: 123} object
|
|
193
168
|
return response as APIResponse<{ data: any[]; lastUpdated: number }>;
|
|
194
169
|
}
|
|
195
170
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const VERBOSE = true;
|
|
2
|
+
|
|
3
|
+
interface TenantResolution {
|
|
4
|
+
id: string;
|
|
5
|
+
source: 'regex' | 'cache' | 'fetch' | 'default';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface CacheEntry {
|
|
9
|
+
tenantId: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const domainCache = new Map<string, CacheEntry>();
|
|
14
|
+
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
15
|
+
|
|
16
|
+
export async function resolveTenantId(
|
|
17
|
+
request: Request
|
|
18
|
+
): Promise<TenantResolution> {
|
|
19
|
+
const hostname =
|
|
20
|
+
request.headers.get('x-forwarded-host') || request.headers.get('host');
|
|
21
|
+
|
|
22
|
+
if (VERBOSE) console.log(`[TenantResolver] Resolving: ${hostname}`);
|
|
23
|
+
|
|
24
|
+
if (!hostname) return { id: 'default', source: 'default' };
|
|
25
|
+
|
|
26
|
+
// Strategy 1: Regex Pattern (Fastest - Zero Latency)
|
|
27
|
+
const parts = hostname.split('.');
|
|
28
|
+
|
|
29
|
+
// Standard Subdomain (e.g. pro.freewebpress.com or pro.freewebpress.com:443)
|
|
30
|
+
if (
|
|
31
|
+
parts.length === 3 &&
|
|
32
|
+
['freewebpress', 'tractstack'].includes(parts[1]) &&
|
|
33
|
+
parts[2].startsWith('com')
|
|
34
|
+
) {
|
|
35
|
+
if (VERBOSE) console.log(`[TenantResolver] Regex Match: ${parts[0]}`);
|
|
36
|
+
return { id: parts[0], source: 'regex' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Sandbox Subdomain (e.g. id.sandbox.freewebpress.com)
|
|
40
|
+
if (
|
|
41
|
+
parts.length >= 4 &&
|
|
42
|
+
parts[1] === 'sandbox' &&
|
|
43
|
+
['freewebpress', 'tractstack'].includes(parts[2]) &&
|
|
44
|
+
parts[3].startsWith('com')
|
|
45
|
+
) {
|
|
46
|
+
if (VERBOSE)
|
|
47
|
+
console.log(`[TenantResolver] Regex Sandbox Match: ${parts[0]}`);
|
|
48
|
+
return { id: parts[0], source: 'regex' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Strategy 2: Cache Lookup (Fast - In Memory)
|
|
52
|
+
const cached = domainCache.get(hostname);
|
|
53
|
+
if (cached) {
|
|
54
|
+
if (Date.now() - cached.timestamp < CACHE_TTL) {
|
|
55
|
+
if (VERBOSE)
|
|
56
|
+
console.log(`[TenantResolver] Cache Hit: ${cached.tenantId}`);
|
|
57
|
+
return { id: cached.tenantId, source: 'cache' };
|
|
58
|
+
} else {
|
|
59
|
+
domainCache.delete(hostname);
|
|
60
|
+
if (VERBOSE)
|
|
61
|
+
console.log(`[TenantResolver] Cache Expired for: ${hostname}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Strategy 3: Backend Lookup (Fallback - Network Request)
|
|
66
|
+
try {
|
|
67
|
+
const backendUrl =
|
|
68
|
+
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:10000';
|
|
69
|
+
const urlObj = new URL(backendUrl);
|
|
70
|
+
// Force localhost to avoid Hairpin NAT / Loopback firewall blocks
|
|
71
|
+
const localBackend = `${urlObj.protocol}//127.0.0.1:${urlObj.port}`;
|
|
72
|
+
|
|
73
|
+
if (VERBOSE) console.log(`[TenantResolver] Fetching from: ${localBackend}`);
|
|
74
|
+
|
|
75
|
+
// Temporarily disable TLS validation because 127.0.0.1 won't match the cert
|
|
76
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
77
|
+
|
|
78
|
+
const response = await fetch(
|
|
79
|
+
`${localBackend}/api/v1/resolve-domain?host=${encodeURIComponent(hostname)}`
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Restore security immediately
|
|
83
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
|
|
84
|
+
|
|
85
|
+
if (response.ok) {
|
|
86
|
+
const data = await response.json();
|
|
87
|
+
if (data.tenantId) {
|
|
88
|
+
if (VERBOSE)
|
|
89
|
+
console.log(`[TenantResolver] Fetch Success: ${data.tenantId}`);
|
|
90
|
+
|
|
91
|
+
// Cache the result
|
|
92
|
+
domainCache.set(hostname, {
|
|
93
|
+
tenantId: data.tenantId,
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return { id: data.tenantId, source: 'fetch' };
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
if (VERBOSE)
|
|
101
|
+
console.log(`[TenantResolver] Fetch Failed: ${response.status}`);
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
|
|
105
|
+
console.error(
|
|
106
|
+
`[TenantResolver] Error resolving domain ${hostname}:`,
|
|
107
|
+
error
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (VERBOSE) console.warn(`[TenantResolver] Failed to resolve. Defaulting.`);
|
|
112
|
+
return { id: 'default', source: 'default' };
|
|
113
|
+
}
|
package/utils/inject-files.ts
CHANGED
|
@@ -758,6 +758,10 @@ export async function injectTemplateFiles(
|
|
|
758
758
|
src: resolve('../templates/src/utils/api.ts'),
|
|
759
759
|
dest: 'src/utils/api.ts',
|
|
760
760
|
},
|
|
761
|
+
{
|
|
762
|
+
src: resolve('../templates/src/utils/tenantResolver.ts'),
|
|
763
|
+
dest: 'src/utils/tenantResolver.ts',
|
|
764
|
+
},
|
|
761
765
|
{
|
|
762
766
|
src: resolve('../templates/src/utils/api/brandConfig.ts'),
|
|
763
767
|
dest: 'src/utils/api/brandConfig.ts',
|