camox 0.35.0 → 0.36.1

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.
@@ -1,8 +1,142 @@
1
- import "../routes/pageRuntime.js";
2
- import { QueryClient } from "@tanstack/react-query";
3
- import "@unhead/react/server";
1
+ import { buildCamoxPageHead, createMarkdownResponse, createServerApiClient, isNotFoundError, loadCamoxPageForRequest } from "../routes/pageRuntime.js";
2
+ import { QueryClient, dehydrate } from "@tanstack/react-query";
3
+ import { createHead, renderSSRHead } from "@unhead/react/server";
4
4
 
5
5
  //#region src/features/runtime/runtime.ts
6
+ const DEFAULT_RUNTIME_BASE_PATH = "";
7
+ const RUNTIME_HEALTH_PATH = "/_camox/health";
8
+ const RUNTIME_REGISTRY_PATH = "/_camox/registry";
9
+ const SERVER_AUTH_COOKIE_NAME = "camox_auth_cookie";
10
+ function buildClearServerAuthCookieHeader() {
11
+ return `${SERVER_AUTH_COOKIE_NAME}=; Path=/; SameSite=Lax; Max-Age=0`;
12
+ }
13
+ function getServerAuthCookieHeader(headers) {
14
+ const authCookie = (headers.get("Cookie") ?? headers.get("cookie") ?? "").split(";").map((part) => part.trim()).find((part) => part.startsWith(`${SERVER_AUTH_COOKIE_NAME}=`));
15
+ if (!authCookie) return "";
16
+ const value = authCookie.slice(18);
17
+ try {
18
+ return decodeURIComponent(value);
19
+ } catch {
20
+ return "";
21
+ }
22
+ }
23
+ function normalizeRuntimeBasePath(basePath) {
24
+ if (basePath === void 0) return DEFAULT_RUNTIME_BASE_PATH;
25
+ if (!basePath || basePath === "/") return "";
26
+ return basePath.startsWith("/") ? basePath.replace(/\/+$/, "") : `/${basePath.replace(/\/+$/, "")}`;
27
+ }
28
+ function getRuntimePathname(url, runtimeBasePath = DEFAULT_RUNTIME_BASE_PATH) {
29
+ const { pathname } = new URL(url);
30
+ const basePath = normalizeRuntimeBasePath(runtimeBasePath);
31
+ if (!basePath) return pathname || "/";
32
+ if (pathname === basePath) return "/";
33
+ if (!pathname.startsWith(`${basePath}/`)) return null;
34
+ return pathname.slice(basePath.length) || "/";
35
+ }
36
+ function escapeXml(value) {
37
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
38
+ }
39
+ function normalizePagePath(path) {
40
+ if (!path) return "/";
41
+ return path.startsWith("/") ? path : `/${path}`;
42
+ }
43
+ function getLayoutIdentity(data, source) {
44
+ const layout = data.page.layout;
45
+ if (!layout) return null;
46
+ const version = source === "live" ? String(layout.livePublishedCheckpointId ?? "none") : String(layout.contentUpdatedAt ?? layout.updatedAt);
47
+ return {
48
+ blockIds: [...layout.beforeBlockIds, ...layout.afterBlockIds],
49
+ id: layout.id,
50
+ layoutId: layout.layoutId,
51
+ version
52
+ };
53
+ }
54
+ function matchRuntimeRoute(pathname) {
55
+ if (pathname === RUNTIME_HEALTH_PATH) return {
56
+ kind: "health",
57
+ pathname
58
+ };
59
+ if (pathname === RUNTIME_REGISTRY_PATH) return {
60
+ kind: "registry",
61
+ pathname
62
+ };
63
+ if (pathname === "/_camox/data") return {
64
+ kind: "data",
65
+ pathname
66
+ };
67
+ if (pathname === "/camox") return {
68
+ kind: "studio",
69
+ pathname
70
+ };
71
+ if (pathname === "/camox/content") return {
72
+ kind: "studio-content",
73
+ pathname
74
+ };
75
+ if (pathname.startsWith("/camox/")) return {
76
+ kind: "studio-nested",
77
+ pathname
78
+ };
79
+ if (pathname === "/og") return {
80
+ kind: "og",
81
+ pathname
82
+ };
83
+ if (pathname === "/sitemap.xml") return {
84
+ kind: "sitemap",
85
+ pathname
86
+ };
87
+ return {
88
+ kind: "page",
89
+ pathname
90
+ };
91
+ }
92
+ function withRuntimeBasePath(pathname, runtimeBasePath) {
93
+ const basePath = normalizeRuntimeBasePath(runtimeBasePath);
94
+ if (!basePath) return pathname;
95
+ if (pathname === "/") return `${basePath}/`;
96
+ return `${basePath}${pathname}`;
97
+ }
98
+ async function createSitemapResponse(request, options) {
99
+ if (!options.apiUrl || !options.projectSlug) return Response.json({ message: "Camox sitemap is not configured." }, { status: 500 });
100
+ const api = createServerApiClient(options.apiUrl, options.environmentName);
101
+ const origin = new URL(request.url).origin;
102
+ const entries = (await api.pages.listBySlug({ projectSlug: options.projectSlug })).map((page) => ` <url>
103
+ <loc>${escapeXml(`${origin}${withRuntimeBasePath(page.fullPath, options.runtimeBasePath)}`)}</loc>
104
+ <lastmod>${escapeXml(new Date(page.updatedAt).toISOString())}</lastmod>
105
+ </url>`).join("\n");
106
+ return new Response(`<?xml version="1.0" encoding="UTF-8"?>
107
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
108
+ ${entries}
109
+ </urlset>`, { headers: { "Content-Type": "application/xml; charset=utf-8" } });
110
+ }
111
+ async function createOgResponse(request, options) {
112
+ const camoxApp = await options.getCamoxApp?.();
113
+ if (!camoxApp) return Response.json({ message: "Camox app registry is not available." }, { status: 500 });
114
+ const url = new URL(request.url);
115
+ const layoutId = url.searchParams.get("layoutId") || "";
116
+ const title = url.searchParams.get("title") || "";
117
+ const description = url.searchParams.get("description") || "";
118
+ const projectName = url.searchParams.get("projectName") || "";
119
+ const layout = camoxApp.getLayoutById(layoutId);
120
+ if (!layout?._internal.buildOgImage) return new Response("Not found", { status: 404 });
121
+ return layout._internal.buildOgImage({
122
+ title,
123
+ description,
124
+ projectName
125
+ });
126
+ }
127
+ function createDefaultHeadInput(stylesheetUrl) {
128
+ return {
129
+ htmlAttrs: { lang: "en" },
130
+ meta: [{ charset: "utf-8" }, {
131
+ name: "viewport",
132
+ content: "width=device-width, initial-scale=1"
133
+ }],
134
+ link: stylesheetUrl ? [{
135
+ rel: "stylesheet",
136
+ href: stylesheetUrl
137
+ }] : []
138
+ };
139
+ }
6
140
  function createPageHeadInput(head) {
7
141
  const meta = head.meta ?? [];
8
142
  const links = head.links ?? [];
@@ -39,6 +173,211 @@ function createPageHeadInput(head) {
39
173
  link: pageLinks
40
174
  };
41
175
  }
176
+ function renderHead(document, pageHead, stylesheetUrl) {
177
+ const head = createHead();
178
+ head.push(createDefaultHeadInput(stylesheetUrl));
179
+ head.push(document);
180
+ head.push(pageHead);
181
+ return renderSSRHead(head);
182
+ }
183
+ function serializeJsonForHtml(value) {
184
+ return JSON.stringify(value).replaceAll("<", "\\u003c");
185
+ }
186
+ function renderClientEntryScript(src) {
187
+ if (!src) return "";
188
+ return `<script type="module" src="${escapeXml(src)}"><\/script>`;
189
+ }
190
+ async function createPageHtmlResponse({ options, pathname, request }) {
191
+ if (!options.apiUrl || !options.authenticationUrl || !options.projectSlug || !options.renderPage) return createPageDataResponse({
192
+ options,
193
+ pathname,
194
+ request
195
+ });
196
+ const queryClient = new QueryClient();
197
+ try {
198
+ const result = await loadCamoxPageForRequest({
199
+ apiUrl: options.apiUrl,
200
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
201
+ environmentName: options.environmentName,
202
+ origin: new URL(request.url).origin,
203
+ pathname,
204
+ projectSlug: options.projectSlug,
205
+ queryClient
206
+ });
207
+ const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
208
+ if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
209
+ const camoxApp = await options.getCamoxApp?.();
210
+ const document = await options.getDocument?.() ?? {};
211
+ const head = camoxApp ? buildCamoxPageHead(camoxApp, result.data) : {};
212
+ const dehydratedState = dehydrate(queryClient);
213
+ const pageRenderInput = {
214
+ apiUrl: options.apiUrl,
215
+ authenticationUrl: options.authenticationUrl,
216
+ dehydratedState,
217
+ environmentName: options.environmentName,
218
+ head,
219
+ href: new URL(request.url).href,
220
+ layoutIdentity: getLayoutIdentity(result.data, result.source),
221
+ loaderData: result.data,
222
+ pathname,
223
+ projectSlug: options.projectSlug,
224
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
225
+ source: result.source
226
+ };
227
+ const appHtml = await options.renderPage(pageRenderInput);
228
+ const renderedHead = renderHead(document, createPageHeadInput(head), options.stylesheetUrl);
229
+ return new Response(`<!doctype html>
230
+ <html${renderedHead.htmlAttrs}>
231
+ <head>
232
+ ${renderedHead.headTags}
233
+ <script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(pageRenderInput)}<\/script>
234
+ </head>
235
+ <body${renderedHead.bodyAttrs}>
236
+ ${renderedHead.bodyTagsOpen}
237
+ <div id="root">${appHtml}</div>
238
+ ${renderedHead.bodyTags}
239
+ ${renderClientEntryScript(options.pageClientEntryUrl)}
240
+ </body>
241
+ </html>`, { headers });
242
+ } catch (error) {
243
+ if (isNotFoundError(error)) return new Response("No Camox page on this path", { status: 404 });
244
+ throw error;
245
+ }
246
+ }
247
+ async function createStudioHtmlResponse({ match, options, request }) {
248
+ if (!options.apiUrl || !options.authenticationUrl || !options.projectSlug || !options.renderStudio) return Response.json({
249
+ message: "Camox Studio renderer is not configured.",
250
+ ...match
251
+ }, { status: 500 });
252
+ const queryClient = new QueryClient();
253
+ const document = await options.getDocument?.() ?? {};
254
+ const studioRenderInput = {
255
+ apiUrl: options.apiUrl,
256
+ authenticationUrl: options.authenticationUrl,
257
+ dehydratedState: dehydrate(queryClient),
258
+ environmentName: options.environmentName,
259
+ href: new URL(request.url).href,
260
+ pathname: match.pathname,
261
+ projectSlug: options.projectSlug,
262
+ routeKind: match.kind,
263
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath)
264
+ };
265
+ const appHtml = await options.renderStudio(studioRenderInput);
266
+ const renderedHead = renderHead(document, {}, options.stylesheetUrl);
267
+ return new Response(`<!doctype html>
268
+ <html${renderedHead.htmlAttrs}>
269
+ <head>
270
+ ${renderedHead.headTags}
271
+ <script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(studioRenderInput)}<\/script>
272
+ </head>
273
+ <body${renderedHead.bodyAttrs}>
274
+ ${renderedHead.bodyTagsOpen}
275
+ <div id="root">${appHtml}</div>
276
+ ${renderedHead.bodyTags}
277
+ ${renderClientEntryScript(options.studioClientEntryUrl)}
278
+ </body>
279
+ </html>`, { headers: { "Content-Type": "text/html; charset=utf-8" } });
280
+ }
281
+ async function createPageDataResponse({ options, pathname, request }) {
282
+ if (!options.apiUrl || !options.projectSlug) return Response.json({ message: "Camox page loader is not configured." }, { status: 500 });
283
+ const queryClient = new QueryClient();
284
+ try {
285
+ const result = await loadCamoxPageForRequest({
286
+ apiUrl: options.apiUrl,
287
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
288
+ environmentName: options.environmentName,
289
+ origin: new URL(request.url).origin,
290
+ pathname,
291
+ projectSlug: options.projectSlug,
292
+ queryClient
293
+ });
294
+ const headers = new Headers({ "Content-Type": "application/json; charset=utf-8" });
295
+ if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
296
+ const camoxApp = await options.getCamoxApp?.();
297
+ const head = camoxApp ? buildCamoxPageHead(camoxApp, result.data) : null;
298
+ const dehydratedState = dehydrate(queryClient);
299
+ const { page, layout, projectName, project } = result.data.page;
300
+ return new Response(`${JSON.stringify({
301
+ kind: "page",
302
+ pathname,
303
+ page: {
304
+ id: page.id,
305
+ fullPath: page.fullPath,
306
+ metaTitle: page.metaTitle,
307
+ metaDescription: page.metaDescription
308
+ },
309
+ layout: layout ? {
310
+ id: layout.id,
311
+ layoutId: layout.layoutId
312
+ } : null,
313
+ layoutIdentity: getLayoutIdentity(result.data, result.source),
314
+ project: {
315
+ id: project.id,
316
+ name: projectName
317
+ },
318
+ loaderData: {
319
+ faviconUrl: result.data.faviconUrl,
320
+ origin: result.data.origin
321
+ },
322
+ head,
323
+ dehydratedState
324
+ }, null, 2)}\n`, { headers });
325
+ } catch (error) {
326
+ if (isNotFoundError(error)) return Response.json({
327
+ kind: "page",
328
+ message: "No Camox page on this path",
329
+ pathname
330
+ }, { status: 404 });
331
+ throw error;
332
+ }
333
+ }
334
+ async function handleCamoxRequest(request, options = {}) {
335
+ const pathname = getRuntimePathname(request.url, options.runtimeBasePath);
336
+ if (!pathname) return null;
337
+ const match = matchRuntimeRoute(pathname);
338
+ if (match.kind === "health") return new Response("ok\n", { headers: { "Content-Type": "text/plain; charset=utf-8" } });
339
+ if (match.kind === "registry") {
340
+ const camoxApp = await options.getCamoxApp?.();
341
+ if (!camoxApp) return Response.json({ message: "Camox app registry is not available." }, { status: 500 });
342
+ return Response.json({
343
+ blocks: camoxApp.getBlocks().map((block) => block._internal.id),
344
+ layouts: camoxApp.getLayouts().map((layout) => layout._internal.id)
345
+ });
346
+ }
347
+ if (match.kind === "og") return createOgResponse(request, options);
348
+ if (match.kind === "sitemap") return createSitemapResponse(request, options);
349
+ if (match.kind === "data") return createPageDataResponse({
350
+ options,
351
+ pathname: normalizePagePath(new URL(request.url).searchParams.get("path")),
352
+ request
353
+ });
354
+ if (match.kind === "studio" || match.kind === "studio-content" || match.kind === "studio-nested") return createStudioHtmlResponse({
355
+ match,
356
+ options,
357
+ request
358
+ });
359
+ if (match.kind === "page") {
360
+ if (options.apiUrl && options.projectSlug) {
361
+ const markdownResponse = await createMarkdownResponse({
362
+ apiUrl: options.apiUrl,
363
+ environmentName: options.environmentName,
364
+ pathname: match.pathname,
365
+ projectSlug: options.projectSlug,
366
+ request
367
+ });
368
+ if (markdownResponse) return markdownResponse;
369
+ }
370
+ return createPageHtmlResponse({
371
+ options,
372
+ pathname: match.pathname,
373
+ request
374
+ });
375
+ }
376
+ return Response.json({
377
+ message: "Camox runtime route matched.",
378
+ ...match
379
+ }, { status: 501 });
380
+ }
42
381
 
43
382
  //#endregion
44
- export { createPageHeadInput };
383
+ export { createPageHeadInput, getRuntimePathname, handleCamoxRequest, matchRuntimeRoute };
@@ -39,9 +39,9 @@ function getStudioTarget(to, runtimeBasePath) {
39
39
  }
40
40
  function StudioNavigationProvider(t0) {
41
41
  const $ = c(19);
42
- if ($[0] !== "df5809d38cc8796db839fa5b8c8d05f562a3e4cba7d51a1fec989dc85db9136b") {
42
+ if ($[0] !== "8131144acb7b9faccf5f5924d863b9186349bfc7c842e7feba7a3ac1e01ed910") {
43
43
  for (let $i = 0; $i < 19; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
44
- $[0] = "df5809d38cc8796db839fa5b8c8d05f562a3e4cba7d51a1fec989dc85db9136b";
44
+ $[0] = "8131144acb7b9faccf5f5924d863b9186349bfc7c842e7feba7a3ac1e01ed910";
45
45
  }
46
46
  const { children, href, pathname, runtimeBasePath } = t0;
47
47
  let t1;
@@ -53,6 +53,11 @@ function StudioNavigationProvider(t0) {
53
53
  window.location.assign(to);
54
54
  return;
55
55
  }
56
+ const currentPathname = normalizeRuntimePathname(window.location.pathname, runtimeBasePath);
57
+ if (normalizeRuntimePathname(target.pathname, runtimeBasePath) !== currentPathname) {
58
+ window.location.assign(target.href);
59
+ return;
60
+ }
56
61
  if (replace) window.history.replaceState(null, "", target.href);
57
62
  else window.history.pushState(null, "", target.href);
58
63
  window.dispatchEvent(new Event("camox:navigation"));
@@ -36,9 +36,9 @@ const links = [{
36
36
  }];
37
37
  const Navbar = () => {
38
38
  const $ = c(19);
39
- if ($[0] !== "9b1adcbc6464388cc572701469e917a5652b82c7ccc4e66be74fb1820176f49e") {
39
+ if ($[0] !== "4fd5828728ac76dc7c63a1b38dbea3991a6750be46540e90d91c5dccb46797e2") {
40
40
  for (let $i = 0; $i < 19; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
41
- $[0] = "9b1adcbc6464388cc572701469e917a5652b82c7ccc4e66be74fb1820176f49e";
41
+ $[0] = "4fd5828728ac76dc7c63a1b38dbea3991a6750be46540e90d91c5dccb46797e2";
42
42
  }
43
43
  const projectSlug = useProjectSlug();
44
44
  let t0;
@@ -136,7 +136,7 @@ const Navbar = () => {
136
136
  let t10;
137
137
  if ($[17] !== t6) {
138
138
  t10 = /* @__PURE__ */ jsxs("nav", {
139
- className: "relative flex items-center justify-between gap-4 border-b-2 bg-transparent px-2 py-2",
139
+ className: "bg-background relative flex items-center justify-between gap-4 border-b-2 px-2 py-2",
140
140
  children: [t6, t9]
141
141
  });
142
142
  $[17] = t6;
@@ -146,9 +146,9 @@ const Navbar = () => {
146
146
  };
147
147
  function useNavbarActions() {
148
148
  const $ = c(4);
149
- if ($[0] !== "9b1adcbc6464388cc572701469e917a5652b82c7ccc4e66be74fb1820176f49e") {
149
+ if ($[0] !== "4fd5828728ac76dc7c63a1b38dbea3991a6750be46540e90d91c5dccb46797e2") {
150
150
  for (let $i = 0; $i < 4; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
151
- $[0] = "9b1adcbc6464388cc572701469e917a5652b82c7ccc4e66be74fb1820176f49e";
151
+ $[0] = "4fd5828728ac76dc7c63a1b38dbea3991a6750be46540e90d91c5dccb46797e2";
152
152
  }
153
153
  const navigate = useNavigate();
154
154
  let t0;
@@ -13,9 +13,9 @@ import { Skeleton } from "@camox/ui/skeleton";
13
13
  //#region src/features/studio/components/ProjectMenu.tsx
14
14
  const Favicon = (t0) => {
15
15
  const $ = c(19);
16
- if ($[0] !== "5b6284d7940526e6093811e0cb2663e0916c58abe0f07e36b7140e4a9eb93d0f") {
16
+ if ($[0] !== "d995cbe0a8e25c463e5078777395b1432b84ff5ecc4a4a3cc0e7e416badd244d") {
17
17
  for (let $i = 0; $i < 19; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
18
- $[0] = "5b6284d7940526e6093811e0cb2663e0916c58abe0f07e36b7140e4a9eb93d0f";
18
+ $[0] = "d995cbe0a8e25c463e5078777395b1432b84ff5ecc4a4a3cc0e7e416badd244d";
19
19
  }
20
20
  const { size: t1 } = t0;
21
21
  const size = t1 === void 0 ? 16 : t1;
@@ -113,9 +113,9 @@ const Favicon = (t0) => {
113
113
  };
114
114
  const ProjectMenu = () => {
115
115
  const $ = c(41);
116
- if ($[0] !== "5b6284d7940526e6093811e0cb2663e0916c58abe0f07e36b7140e4a9eb93d0f") {
116
+ if ($[0] !== "d995cbe0a8e25c463e5078777395b1432b84ff5ecc4a4a3cc0e7e416badd244d") {
117
117
  for (let $i = 0; $i < 41; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
118
- $[0] = "5b6284d7940526e6093811e0cb2663e0916c58abe0f07e36b7140e4a9eb93d0f";
118
+ $[0] = "d995cbe0a8e25c463e5078777395b1432b84ff5ecc4a4a3cc0e7e416badd244d";
119
119
  }
120
120
  const authCtx = React.useContext(AuthContext);
121
121
  let t0;
@@ -282,7 +282,7 @@ const ProjectMenu = () => {
282
282
  if ($[34] === Symbol.for("react.memo_cache_sentinel")) {
283
283
  t22 = /* @__PURE__ */ jsxs(DropdownMenuItem, {
284
284
  render: /* @__PURE__ */ jsx(Link$1, {
285
- to: "https://camox.ai",
285
+ to: "https://camox.dev",
286
286
  target: "_blank"
287
287
  }),
288
288
  children: [/* @__PURE__ */ jsx(Info, { className: "h-4 w-4" }), "Powered by Camox"]
@@ -28,7 +28,7 @@ interface CamoxPluginOptions {
28
28
  disableTelemetry?: boolean;
29
29
  /** Internal options (intended for Camox contributors in development, not for public use) */
30
30
  _internal?: {
31
- /** URL of the Camox API backend, used for data fetching */apiUrl?: string; /** URL of the Camox authentication backend (default: https://app.camox.ai) */
31
+ /** URL of the Camox API backend, used for data fetching */apiUrl?: string; /** URL of the Camox authentication backend (default: https://app.camox.dev) */
32
32
  authenticationUrl?: string; /** Show Tanstack query devtools (default: false) */
33
33
  enableTanstackDevtools?: boolean; /** Disable automatic code generation (route files, app file, skill files) (default: false) */
34
34
  disableCodeGen?: boolean; /** Mount the Camox-owned dev runtime at this base path. Use "/" for root mode. */
@@ -21,9 +21,9 @@ const VIRTUAL_PAGE_CLIENT = "virtual:camox/page-client";
21
21
  const RESOLVED_VIRTUAL_PAGE_CLIENT_URL = "\0virtual:camox/page-client-url";
22
22
  const VIRTUAL_STUDIO_CLIENT = "virtual:camox/studio-client";
23
23
  const RESOLVED_VIRTUAL_STUDIO_CLIENT_URL = "\0virtual:camox/studio-client-url";
24
- const PRODUCTION_API_URL = "https://api.camox.ai";
24
+ const PRODUCTION_API_URL = "https://api.camox.dev";
25
25
  /** Authentication URL to use for Camox authentication (production Camox Dashboard) */
26
- const DEFAULT_AUTHENTICATION_URL = "https://app.camox.ai";
26
+ const DEFAULT_AUTHENTICATION_URL = "https://app.camox.dev";
27
27
  const authTokenSchema = z.object({
28
28
  token: z.string(),
29
29
  name: z.string(),
@@ -8,7 +8,7 @@ function ensureInitialized() {
8
8
  if (typeof window === "undefined") return false;
9
9
  if (initialized) return true;
10
10
  posthog.init(POSTHOG_PUBLIC_KEY, {
11
- api_host: "https://t.camox.ai",
11
+ api_host: "https://t.camox.dev",
12
12
  ui_host: "https://us.posthog.com",
13
13
  defaults: "2026-01-30",
14
14
  autocapture: false,
@@ -56,7 +56,7 @@ function identifyProject(options) {
56
56
  slug: options.projectSlug
57
57
  });
58
58
  }
59
- /** Client-side tracking for CMS actions. Events route through t.camox.ai reverse proxy. */
59
+ /** Client-side tracking for CMS actions. Events route through t.camox.dev reverse proxy. */
60
60
  function trackClientEvent(event, properties) {
61
61
  if (!ensureInitialized()) return;
62
62
  posthog.capture(event, properties);
@@ -1,3 +1,26 @@
1
1
  import { POSTHOG_PUBLIC_KEY } from "@camox/api-contract";
2
2
 
3
- export { };
3
+ //#region src/lib/telemetry.ts
4
+ let client = null;
5
+ async function getClient() {
6
+ if (__CAMOX_TELEMETRY_DISABLED__) return null;
7
+ if (!client) {
8
+ const { PostHog } = await import("posthog-node");
9
+ client = new PostHog(POSTHOG_PUBLIC_KEY, { host: "https://us.i.posthog.com" });
10
+ }
11
+ return client;
12
+ }
13
+ /** Server-side tracking for page views (used in pageRoute.tsx) */
14
+ async function trackEvent(event, properties) {
15
+ const posthog = await getClient();
16
+ if (!posthog) return;
17
+ const distinctId = properties.projectId ?? "unknown";
18
+ posthog.capture({
19
+ distinctId,
20
+ event,
21
+ properties
22
+ });
23
+ }
24
+
25
+ //#endregion
26
+ export { trackEvent };