@lowdefy/server-e2e 0.0.0-experimental-20260813120102 → 0.0.0-experimental-20260814133003

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/client/App.jsx CHANGED
@@ -39,7 +39,7 @@ function ThemeTokenResolver({ lowdefyRef, children }) {
39
39
  }
40
40
 
41
41
  function App({ config }) {
42
- const { rootConfig, user } = config;
42
+ const { rootConfig, session } = config;
43
43
 
44
44
  const usageDataRef = useRef({});
45
45
  const lowdefyRef = useRef({ eventCallback: createLogUsage({ usageDataRef }) });
@@ -111,9 +111,9 @@ function App({ config }) {
111
111
  <AntdApp>
112
112
  <ThemeTokenResolver lowdefyRef={lowdefyRef}>
113
113
  <ErrorBoundary fullPage onError={handleError}>
114
- <Auth user={user}>
114
+ <Auth session={session}>
115
115
  {(auth) => {
116
- usageDataRef.current.user = auth.user?.id;
116
+ usageDataRef.current.user = auth.session?.hashed_id;
117
117
  return <Page auth={auth} config={config} lowdefy={lowdefyRef.current} />;
118
118
  }}
119
119
  </Auth>
package/client/Page.jsx CHANGED
@@ -19,7 +19,6 @@ import React, { useEffect, useRef, useState } from 'react';
19
19
  import Client from '@lowdefy/client';
20
20
  import createRouter from '@lowdefy/client/adapters/createRouter.js';
21
21
  import createLinkComponent from '@lowdefy/client/adapters/Link.js';
22
- import { createUrl } from '@lowdefy/client/adapters/url.js';
23
22
  import Head from '@lowdefy/client/adapters/Head.js';
24
23
 
25
24
  import actions from '../build/plugins/actions.js';
@@ -44,35 +43,27 @@ function Page({ auth, config, lowdefy }) {
44
43
  }
45
44
  const { router, Link } = routerRef.current;
46
45
 
47
- // Temporary sequence guard for the superseded-navigation race: a slow
48
- // response for an abandoned navigation must not paint over a newer one.
49
- // Remove when the loader-based lifecycle replaces these fetch paths.
50
- const latestNavRef = useRef(0);
51
-
52
46
  useEffect(() => {
53
47
  const unsubscribe = router.subscribe(async ({ pageId }) => {
54
- const token = ++latestNavRef.current;
55
48
  const targetPageId = pageId ?? config.rootConfig.home.pageId;
56
49
  try {
57
- const res = await fetch(`${router.basePath}/api/page/${targetPageId}`);
50
+ // Forward the current query string so server-side Dynamic block
51
+ // resolution sees the same urlQuery as an initial HTML load.
52
+ const res = await fetch(
53
+ `${router.basePath}/api/page/${targetPageId}${window.location.search}`
54
+ );
58
55
  if (!res.ok) {
59
- if (token !== latestNavRef.current) return;
60
56
  if (targetPageId !== '404') {
61
57
  router.replace({ pathname: '/404' });
62
58
  }
63
59
  return;
64
60
  }
65
61
  const { pageConfig: nextPageConfig } = await res.json();
66
- if (token !== latestNavRef.current) return;
67
62
  setPageConfig(nextPageConfig);
68
63
  } catch (error) {
69
64
  // Network failure on SPA navigation — fall back to a full page load.
70
- if (token !== latestNavRef.current) return;
71
65
  window.location.assign(
72
- createUrl({
73
- basePath: router.basePath,
74
- pathname: targetPageId === config.rootConfig.home.pageId ? '/' : `/${targetPageId}`,
75
- })
66
+ `${router.basePath}/${targetPageId === config.rootConfig.home.pageId ? '' : targetPageId}`
76
67
  );
77
68
  }
78
69
  });
@@ -24,46 +24,19 @@ function e2eNotSupported() {
24
24
  throw new Error('Sign-in and sign-out are not supported in e2e testing.');
25
25
  }
26
26
 
27
- function Auth({ children, user }) {
27
+ function Auth({ children, session }) {
28
28
  const auth = {
29
29
  authConfig,
30
- user,
31
- // Matches the BetterAuth client contract: { data, error }.
30
+ session,
32
31
  getSession: async () => {
33
32
  const res = await fetch('/api/auth/session');
34
- if (res.ok) {
35
- return { data: await res.json(), error: null };
36
- }
37
- return { data: null, error: null };
38
- },
39
- getResolvedUser: async () => {
40
- const res = await fetch('/api/user');
41
33
  if (res.ok) {
42
34
  return res.json();
43
35
  }
44
- return { user: null };
36
+ return null;
45
37
  },
46
- acceptInvitation: e2eNotSupported,
47
- addPasskey: e2eNotSupported,
48
- changePassword: e2eNotSupported,
49
- deletePasskey: e2eNotSupported,
50
- updatePasskey: e2eNotSupported,
51
- requestPasswordReset: e2eNotSupported,
52
- resetPassword: e2eNotSupported,
53
- revokeOtherSessions: e2eNotSupported,
54
- sendVerificationEmail: e2eNotSupported,
55
- setActiveOrganization: e2eNotSupported,
56
- signInEmail: e2eNotSupported,
57
- signInMagicLink: e2eNotSupported,
58
- signInOauth2: e2eNotSupported,
59
- signInSocial: e2eNotSupported,
38
+ signIn: e2eNotSupported,
60
39
  signOut: e2eNotSupported,
61
- signUpEmail: e2eNotSupported,
62
- twoFactorDisable: e2eNotSupported,
63
- twoFactorEnable: e2eNotSupported,
64
- twoFactorGenerateBackupCodes: e2eNotSupported,
65
- twoFactorVerifyBackupCode: e2eNotSupported,
66
- twoFactorVerifyTotp: e2eNotSupported,
67
40
  };
68
41
  return children(auth);
69
42
  }
@@ -14,23 +14,22 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- // E2E test-user injection: the caller is read from the lowdefy_e2e_user
17
+ // E2E test-user injection: the session is read from the lowdefy_e2e_user
18
18
  // cookie (base64-encoded JSON user object) instead of a real auth engine.
19
- // The cookie user is pre-resolved - it substitutes for resolveAuthentication
20
- // and its roles are authoritative.
21
- function getUser(c) {
19
+ function getSession(c) {
22
20
  const cookieHeader = c.req.header('cookie') ?? '';
23
21
  const match = cookieHeader.match(/lowdefy_e2e_user=([^;]+)/);
24
22
  if (!match) {
25
- return null;
23
+ return undefined;
26
24
  }
27
25
 
28
26
  try {
29
27
  const decoded = Buffer.from(decodeURIComponent(match[1]), 'base64').toString();
30
- return JSON.parse(decoded);
28
+ const user = JSON.parse(decoded);
29
+ return { user };
31
30
  } catch {
32
- return null;
31
+ return undefined;
33
32
  }
34
33
  }
35
34
 
36
- export default getUser;
35
+ export default getSession;
@@ -14,7 +14,6 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import { isReserved, setKey } from '@lowdefy/helpers';
18
17
  import { getSecretsFromEnv } from '@lowdefy/node-utils';
19
18
 
20
19
  import createLogger from './log/createLogger.js';
@@ -27,13 +26,7 @@ function getE2eSecrets() {
27
26
  const e2eOverrides = {};
28
27
  Object.keys(process.env).forEach((key) => {
29
28
  if (key.startsWith('LOWDEFY_E2E_SECRET_')) {
30
- const name = key.replace('LOWDEFY_E2E_SECRET_', '');
31
- if (isReserved(name)) {
32
- throw new Error(
33
- `Environment variable "${key}" names a reserved secret "${name}". Rename the secret.`
34
- );
35
- }
36
- setKey(e2eOverrides, name, process.env[key]);
29
+ e2eOverrides[key.replace('LOWDEFY_E2E_SECRET_', '')] = process.env[key];
37
30
  }
38
31
  });
39
32
 
@@ -19,15 +19,6 @@ import { LowdefyInternalError, loadAndResolveErrorLocation } from '@lowdefy/erro
19
19
  function createHandleError({ context }) {
20
20
  return async function handleError(error) {
21
21
  try {
22
- // Set first, not after the log call: this function is the server's error
23
- // sink, so reaching it is what makes the error already-logged, and the catch
24
- // below guarantees something is written even when the steps here fail. The
25
- // client reads `handled` to decide whether to POST the error to
26
- // /api/client-error, so a later throw skipping this assignment would have it
27
- // logged a second time. Not keyed on `source`, which a LowdefyInternalError
28
- // never gets - location resolution is skipped for it below.
29
- error.handled = true;
30
-
31
22
  // For internal lowdefy errors, don't resolve config location
32
23
  const location =
33
24
  error instanceof LowdefyInternalError
@@ -15,11 +15,13 @@
15
15
  */
16
16
 
17
17
  function logRequest({ context }) {
18
- const user = context.user ?? {};
18
+ const { user = {} } = context.session ?? {};
19
19
  context.logger.debug({
20
20
  user: {
21
21
  id: user.id,
22
22
  roles: user.roles,
23
+ sub: user.sub,
24
+ session_id: user.session_id,
23
25
  },
24
26
  url: context.req.url,
25
27
  method: context.req.method,
@@ -42,11 +42,11 @@ async function createCustomPluginTypesMap({ directories }) {
42
42
  agents: {},
43
43
  auth: {
44
44
  adapters: {},
45
+ callbacks: {},
46
+ events: {},
45
47
  providers: {},
46
- strategies: {},
47
48
  },
48
49
  blockMetas: {},
49
- connectionMetas: {},
50
50
  blocks: {},
51
51
  connections: {},
52
52
  icons: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/server-e2e",
3
- "version": "0.0.0-experimental-20260813120102",
3
+ "version": "0.0.0-experimental-20260814133003",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Lowdefy e2e testing server with cookie-based user injection",
6
6
  "homepage": "https://lowdefy.com",
@@ -43,30 +43,29 @@
43
43
  "@ant-design/cssinjs": "2.1.2",
44
44
  "@ant-design/x": "2.7.0",
45
45
  "@hono/node-server": "2.0.4",
46
- "@lowdefy/actions-core": "0.0.0-experimental-20260813120102",
47
- "@lowdefy/api": "0.0.0-experimental-20260813120102",
48
- "@lowdefy/block-utils": "0.0.0-experimental-20260813120102",
49
- "@lowdefy/blocks-aggrid": "0.0.0-experimental-20260813120102",
50
- "@lowdefy/blocks-antd": "0.0.0-experimental-20260813120102",
51
- "@lowdefy/blocks-antd-x": "0.0.0-experimental-20260813120102",
52
- "@lowdefy/blocks-basic": "0.0.0-experimental-20260813120102",
53
- "@lowdefy/blocks-captcha": "0.0.0-experimental-20260813120102",
54
- "@lowdefy/blocks-echarts": "0.0.0-experimental-20260813120102",
55
- "@lowdefy/blocks-loaders": "0.0.0-experimental-20260813120102",
56
- "@lowdefy/blocks-markdown": "0.0.0-experimental-20260813120102",
57
- "@lowdefy/blocks-tiptap": "0.0.0-experimental-20260813120102",
58
- "@lowdefy/client": "0.0.0-experimental-20260813120102",
59
- "@lowdefy/connection-axios-http": "0.0.0-experimental-20260813120102",
60
- "@lowdefy/connection-mongodb": "0.0.0-experimental-20260813120102",
61
- "@lowdefy/errors": "0.0.0-experimental-20260813120102",
62
- "@lowdefy/helpers": "0.0.0-experimental-20260813120102",
63
- "@lowdefy/layout": "0.0.0-experimental-20260813120102",
64
- "@lowdefy/logger": "0.0.0-experimental-20260813120102",
65
- "@lowdefy/node-utils": "0.0.0-experimental-20260813120102",
66
- "@lowdefy/operators-js": "0.0.0-experimental-20260813120102",
67
- "@lowdefy/operators-nunjucks": "0.0.0-experimental-20260813120102",
68
- "@lowdefy/operators-uuid": "0.0.0-experimental-20260813120102",
69
- "@lowdefy/websockets-core": "0.0.0-experimental-20260813120102",
46
+ "@lowdefy/actions-core": "0.0.0-experimental-20260814133003",
47
+ "@lowdefy/api": "0.0.0-experimental-20260814133003",
48
+ "@lowdefy/block-utils": "0.0.0-experimental-20260814133003",
49
+ "@lowdefy/blocks-aggrid": "0.0.0-experimental-20260814133003",
50
+ "@lowdefy/blocks-antd": "0.0.0-experimental-20260814133003",
51
+ "@lowdefy/blocks-antd-x": "0.0.0-experimental-20260814133003",
52
+ "@lowdefy/blocks-basic": "0.0.0-experimental-20260814133003",
53
+ "@lowdefy/blocks-echarts": "0.0.0-experimental-20260814133003",
54
+ "@lowdefy/blocks-loaders": "0.0.0-experimental-20260814133003",
55
+ "@lowdefy/blocks-markdown": "0.0.0-experimental-20260814133003",
56
+ "@lowdefy/blocks-tiptap": "0.0.0-experimental-20260814133003",
57
+ "@lowdefy/client": "0.0.0-experimental-20260814133003",
58
+ "@lowdefy/connection-axios-http": "0.0.0-experimental-20260814133003",
59
+ "@lowdefy/connection-mongodb": "0.0.0-experimental-20260814133003",
60
+ "@lowdefy/errors": "0.0.0-experimental-20260814133003",
61
+ "@lowdefy/helpers": "0.0.0-experimental-20260814133003",
62
+ "@lowdefy/layout": "0.0.0-experimental-20260814133003",
63
+ "@lowdefy/logger": "0.0.0-experimental-20260814133003",
64
+ "@lowdefy/node-utils": "0.0.0-experimental-20260814133003",
65
+ "@lowdefy/operators-js": "0.0.0-experimental-20260814133003",
66
+ "@lowdefy/operators-nunjucks": "0.0.0-experimental-20260814133003",
67
+ "@lowdefy/operators-uuid": "0.0.0-experimental-20260814133003",
68
+ "@lowdefy/websockets-core": "0.0.0-experimental-20260814133003",
70
69
  "@tailwindcss/postcss": "4.3.1",
71
70
  "@vitejs/plugin-react": "6.0.2",
72
71
  "antd": "6.3.1",
@@ -83,7 +82,7 @@
83
82
  },
84
83
  "devDependencies": {
85
84
  "@jest/globals": "28.1.3",
86
- "@lowdefy/build": "0.0.0-experimental-20260813120102",
85
+ "@lowdefy/build": "0.0.0-experimental-20260814133003",
87
86
  "@next/eslint-plugin-next": "16.1.6",
88
87
  "@tailwindcss/postcss": "4.3.1",
89
88
  "jest": "28.1.3",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/server-e2e",
3
- "version": "0.0.0-experimental-20260813120102",
3
+ "version": "0.0.0-experimental-20260814133003",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Lowdefy e2e testing server with cookie-based user injection",
6
6
  "homepage": "https://lowdefy.com",
@@ -51,30 +51,29 @@
51
51
  "@ant-design/cssinjs": "2.1.2",
52
52
  "@ant-design/x": "2.7.0",
53
53
  "@hono/node-server": "2.0.4",
54
- "@lowdefy/actions-core": "0.0.0-experimental-20260813120102",
55
- "@lowdefy/api": "0.0.0-experimental-20260813120102",
56
- "@lowdefy/block-utils": "0.0.0-experimental-20260813120102",
57
- "@lowdefy/blocks-aggrid": "0.0.0-experimental-20260813120102",
58
- "@lowdefy/blocks-antd": "0.0.0-experimental-20260813120102",
59
- "@lowdefy/blocks-antd-x": "0.0.0-experimental-20260813120102",
60
- "@lowdefy/blocks-basic": "0.0.0-experimental-20260813120102",
61
- "@lowdefy/blocks-captcha": "0.0.0-experimental-20260813120102",
62
- "@lowdefy/blocks-echarts": "0.0.0-experimental-20260813120102",
63
- "@lowdefy/blocks-loaders": "0.0.0-experimental-20260813120102",
64
- "@lowdefy/blocks-markdown": "0.0.0-experimental-20260813120102",
65
- "@lowdefy/blocks-tiptap": "0.0.0-experimental-20260813120102",
66
- "@lowdefy/client": "0.0.0-experimental-20260813120102",
67
- "@lowdefy/connection-axios-http": "0.0.0-experimental-20260813120102",
68
- "@lowdefy/connection-mongodb": "0.0.0-experimental-20260813120102",
69
- "@lowdefy/errors": "0.0.0-experimental-20260813120102",
70
- "@lowdefy/helpers": "0.0.0-experimental-20260813120102",
71
- "@lowdefy/layout": "0.0.0-experimental-20260813120102",
72
- "@lowdefy/logger": "0.0.0-experimental-20260813120102",
73
- "@lowdefy/node-utils": "0.0.0-experimental-20260813120102",
74
- "@lowdefy/operators-js": "0.0.0-experimental-20260813120102",
75
- "@lowdefy/operators-nunjucks": "0.0.0-experimental-20260813120102",
76
- "@lowdefy/operators-uuid": "0.0.0-experimental-20260813120102",
77
- "@lowdefy/websockets-core": "0.0.0-experimental-20260813120102",
54
+ "@lowdefy/actions-core": "0.0.0-experimental-20260814133003",
55
+ "@lowdefy/api": "0.0.0-experimental-20260814133003",
56
+ "@lowdefy/block-utils": "0.0.0-experimental-20260814133003",
57
+ "@lowdefy/blocks-aggrid": "0.0.0-experimental-20260814133003",
58
+ "@lowdefy/blocks-antd": "0.0.0-experimental-20260814133003",
59
+ "@lowdefy/blocks-antd-x": "0.0.0-experimental-20260814133003",
60
+ "@lowdefy/blocks-basic": "0.0.0-experimental-20260814133003",
61
+ "@lowdefy/blocks-echarts": "0.0.0-experimental-20260814133003",
62
+ "@lowdefy/blocks-loaders": "0.0.0-experimental-20260814133003",
63
+ "@lowdefy/blocks-markdown": "0.0.0-experimental-20260814133003",
64
+ "@lowdefy/blocks-tiptap": "0.0.0-experimental-20260814133003",
65
+ "@lowdefy/client": "0.0.0-experimental-20260814133003",
66
+ "@lowdefy/connection-axios-http": "0.0.0-experimental-20260814133003",
67
+ "@lowdefy/connection-mongodb": "0.0.0-experimental-20260814133003",
68
+ "@lowdefy/errors": "0.0.0-experimental-20260814133003",
69
+ "@lowdefy/helpers": "0.0.0-experimental-20260814133003",
70
+ "@lowdefy/layout": "0.0.0-experimental-20260814133003",
71
+ "@lowdefy/logger": "0.0.0-experimental-20260814133003",
72
+ "@lowdefy/node-utils": "0.0.0-experimental-20260814133003",
73
+ "@lowdefy/operators-js": "0.0.0-experimental-20260814133003",
74
+ "@lowdefy/operators-nunjucks": "0.0.0-experimental-20260814133003",
75
+ "@lowdefy/operators-uuid": "0.0.0-experimental-20260814133003",
76
+ "@lowdefy/websockets-core": "0.0.0-experimental-20260814133003",
78
77
  "@tailwindcss/postcss": "4.3.1",
79
78
  "@vitejs/plugin-react": "6.0.2",
80
79
  "antd": "6.3.1",
@@ -91,7 +90,7 @@
91
90
  },
92
91
  "devDependencies": {
93
92
  "@jest/globals": "28.1.3",
94
- "@lowdefy/build": "0.0.0-experimental-20260813120102",
93
+ "@lowdefy/build": "0.0.0-experimental-20260814133003",
95
94
  "@next/eslint-plugin-next": "16.1.6",
96
95
  "@tailwindcss/postcss": "4.3.1",
97
96
  "jest": "28.1.3",
package/src/app.js CHANGED
@@ -29,7 +29,6 @@ import renderPage from './html/renderPage.js';
29
29
  import requestHandler from './routes/request.js';
30
30
  import sessionMockHandler from './routes/sessionMock.js';
31
31
  import usageHandler from './routes/usage.js';
32
- import userHandler from './routes/user.js';
33
32
  import websocketHandler from './routes/websocket.js';
34
33
 
35
34
  const basePath = lowdefyConfig.basePath ?? '';
@@ -55,7 +54,6 @@ function createApp() {
55
54
  app.all('/api/usage', usageHandler);
56
55
  app.get('/api/websocket', websocketHandler);
57
56
  app.get('/api/page/*', apiPageHandler);
58
- app.get('/api/user', userHandler);
59
57
 
60
58
  // Vite build output (includes public/ via Vite's publicDir copy). Falls
61
59
  // through to the page routes when no file matches.
@@ -28,7 +28,7 @@ const basePath = lowdefyConfig.basePath ?? '';
28
28
  // getStaticProps. The home redirect logic lives here, not at the route level.
29
29
  async function renderPage(c, { pageId, status = 200 }) {
30
30
  const context = c.get('lowdefyContext');
31
- const { logger, user } = context;
31
+ const { logger, session } = context;
32
32
 
33
33
  let resolvedPageId = pageId;
34
34
  const rootConfig = await getRootConfig(context);
@@ -42,9 +42,12 @@ async function renderPage(c, { pageId, status = 200 }) {
42
42
  resolvedPageId = home.pageId;
43
43
  }
44
44
 
45
- const result = await getPageConfig(context, { pageId: resolvedPageId });
45
+ const pageConfig = await getPageConfig(context, {
46
+ pageId: resolvedPageId,
47
+ urlQuery: c.req.query(),
48
+ });
46
49
 
47
- if (result.status !== 'ok') {
50
+ if (!pageConfig) {
48
51
  if (resolvedPageId === '404') {
49
52
  // No 404 page in the build — return a plain 404 rather than redirecting in a loop.
50
53
  return c.text('Page not found.', 404);
@@ -53,8 +56,6 @@ async function renderPage(c, { pageId, status = 200 }) {
53
56
  return c.redirect(`${basePath}/404`, 302);
54
57
  }
55
58
 
56
- const { pageConfig } = result;
57
-
58
59
  logger.info({ event: 'page_view', pageId: resolvedPageId });
59
60
 
60
61
  const html = template({
@@ -66,7 +67,7 @@ async function renderPage(c, { pageId, status = 200 }) {
66
67
  basePath,
67
68
  pageConfig,
68
69
  rootConfig,
69
- user: user ?? null,
70
+ session: session ?? null,
70
71
  },
71
72
  themeConfig,
72
73
  title: pageConfig.properties?.title ?? resolvedPageId,
@@ -30,16 +30,15 @@ function escapeHtml(value) {
30
30
  // cascade order.
31
31
  const layerOrderScript = `(function(){var s=document.createElement("style");s.id="__lf-layer-order";s.textContent="@layer theme, base, antd, components, utilities;";document.head.prepend(s);new MutationObserver(function(){if(document.head.firstChild!==s)document.head.prepend(s)}).observe(document.head,{childList:true})})();`;
32
32
 
33
- // Synchronous pre-hydration script setting the <html> background and
34
- // color-scheme — prevents a mode-mismatch flash of the page and of browser
35
- // chrome (scrollbars, form controls) on page load. Mirrors useDarkMode.js
36
- // resolution order: configDarkMode → localStorage → prefers-color-scheme.
33
+ // Synchronous pre-hydration background script — prevents mode-mismatch flash
34
+ // on page load. Mirrors useDarkMode.js resolution order:
35
+ // configDarkMode → localStorage → prefers-color-scheme.
37
36
  function darkModeScript({ configColorMode, darkBg, lightBg }) {
38
37
  return `(function(){var c=${safeScriptJson(configColorMode)};var db=${safeScriptJson(
39
38
  darkBg
40
39
  )};var lb=${safeScriptJson(
41
40
  lightBg
42
- )};var d;if(c==="dark")d=true;else if(c==="light")d=false;else{try{var p=localStorage.getItem("lowdefy_darkMode");if(p==="dark")d=true;else if(p==="light")d=false;else d=window.matchMedia("(prefers-color-scheme:dark)").matches}catch(e){d=window.matchMedia("(prefers-color-scheme:dark)").matches}}var bg=d?db:lb;document.documentElement.style.colorScheme=d?"dark":"light";if(bg)document.documentElement.style.backgroundColor=bg})();`;
41
+ )};var d;if(c==="dark")d=true;else if(c==="light")d=false;else{try{var p=localStorage.getItem("lowdefy_darkMode");if(p==="dark")d=true;else if(p==="light")d=false;else d=window.matchMedia("(prefers-color-scheme:dark)").matches}catch(e){d=window.matchMedia("(prefers-color-scheme:dark)").matches}}var bg=d?db:lb;if(bg)document.documentElement.style.backgroundColor=bg})();`;
43
42
  }
44
43
 
45
44
  function template({
package/src/index.js CHANGED
@@ -18,16 +18,14 @@ import { serve } from '@hono/node-server';
18
18
  import { WebSocketServer } from 'ws';
19
19
 
20
20
  import createApp from './app.js';
21
- import createLogger from '../lib/server/log/createLogger.js';
22
21
 
23
22
  const app = createApp();
24
- const logger = createLogger();
25
23
  const port = Number(process.env.PORT ?? 3000);
26
24
 
27
25
  const wss = new WebSocketServer({ noServer: true, maxPayload: 256 * 1024 });
28
26
 
29
27
  const server = serve({ fetch: app.fetch, port, websocket: { server: wss } }, (info) => {
30
- logger.info(`Lowdefy e2e server listening on http://localhost:${info.port}`);
28
+ console.log(`Lowdefy e2e server listening on http://localhost:${info.port}`);
31
29
  });
32
30
 
33
31
  function shutdown() {
@@ -15,24 +15,22 @@
15
15
  */
16
16
 
17
17
  import path from 'node:path';
18
- import { createApiContext, normalizeInjectedCaller } from '@lowdefy/api';
18
+ import { createApiContext } from '@lowdefy/api';
19
19
  import { v4 as uuid } from 'uuid';
20
20
 
21
21
  import agents from '../../build/plugins/agents.js';
22
22
  import appMeta from '../../lib/build/appMeta.js';
23
- import authJson from '../../lib/build/auth.js';
24
23
  import config from '../../lib/build/config.js';
25
24
  import connections from '../../build/plugins/connections.js';
26
25
  import createHandleError from '../../lib/server/log/createHandleError.js';
27
26
  import createLogger from '../../lib/server/log/createLogger.js';
28
27
  import fileCache from '../../lib/server/fileCache.js';
29
28
  import getE2eSecrets from '../../lib/server/getE2eSecrets.js';
30
- import getUser from '../../lib/server/auth/getUser.js';
29
+ import getSession from '../../lib/server/auth/session.js';
31
30
  import i18nConfig from '../../lib/build/i18n.js';
32
31
  import jsMap from '../../build/plugins/operators/serverJsMap.js';
33
32
  import logRequest from '../../lib/server/log/logRequest.js';
34
33
  import operators from '../../build/plugins/operators/server.js';
35
- import steps from '../../build/plugins/steps.js';
36
34
  import websockets from '../../build/plugins/websockets.js';
37
35
 
38
36
  const secrets = getE2eSecrets();
@@ -48,9 +46,8 @@ function apiContext() {
48
46
  if (c.get('lowdefyContext')) {
49
47
  return next();
50
48
  }
51
- const rid = uuid();
52
49
  const context = {
53
- rid,
50
+ rid: uuid(),
54
51
  agents,
55
52
  appMeta,
56
53
  buildDirectory: path.join(process.cwd(), 'build'),
@@ -61,7 +58,10 @@ function apiContext() {
61
58
  headers: c.req.header(),
62
59
  i18n: i18nConfig,
63
60
  jsMap,
64
- logger: createLogger({ rid }),
61
+ handleError: async (err) => {
62
+ console.error(err);
63
+ },
64
+ logger: console,
65
65
  operators,
66
66
  req: {
67
67
  url: c.req.path,
@@ -69,23 +69,12 @@ function apiContext() {
69
69
  hostname: c.req.header('host'),
70
70
  },
71
71
  secrets,
72
- steps,
73
72
  websockets,
74
73
  };
74
+ context.logger = createLogger({ rid: context.rid });
75
75
  context.handleError = createHandleError({ context });
76
- // The cookie user is a pre-resolved caller substituting for
77
- // resolveAuthentication - normalizeInjectedCaller floors it to the
78
- // resolved-caller shape. An absent cookie stays a logged-out (null) caller.
79
- const user = getUser(c);
80
- context.user = user ? normalizeInjectedCaller(user) : null;
76
+ context.session = getSession(c);
81
77
  createApiContext(context);
82
- // No auth engine runs in the e2e server, so createApiContext retains no
83
- // organization binding. Derive the policy from the built auth config so
84
- // the tenant wall's policy gate still engages for a tenant app; pinned
85
- // stays null (no engine means no seeded organization row).
86
- if (authJson.organizations) {
87
- context.organization = { policy: authJson.organizations.policy, pinned: null };
88
- }
89
78
  logRequest({ context });
90
79
  c.set('lowdefyContext', context);
91
80
  return next();
@@ -14,7 +14,7 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import { redactErrorResponse } from '@lowdefy/api';
17
+ import { serializer } from '@lowdefy/helpers';
18
18
 
19
19
  // Hono routes every handler error to the app-level error handler — upstream
20
20
  // middleware try/catch never sees them (compose catches per dispatch level).
@@ -30,7 +30,13 @@ function createErrorHandler({ basePath = '', logger }) {
30
30
  }
31
31
  const path = basePath ? c.req.path.replace(basePath, '') : c.req.path;
32
32
  if (path.startsWith('/api/')) {
33
- return c.json(redactErrorResponse(context, error), 500);
33
+ const serialized = serializer.serialize(error);
34
+ if (serialized?.['~e']) {
35
+ delete serialized['~e'].received;
36
+ delete serialized['~e'].stack;
37
+ delete serialized['~e'].configKey;
38
+ }
39
+ return c.json(serialized, 500);
34
40
  }
35
41
  return c.text('Internal Server Error', 500);
36
42
  };
@@ -23,13 +23,15 @@ import getPathSegments from '../lib/getPathSegments.js';
23
23
  async function apiPageHandler(c) {
24
24
  const context = c.get('lowdefyContext');
25
25
  const pageId = getPathSegments(c, '/api/page/').join('/');
26
- const result = await getPageConfig(context, { pageId });
27
- if (result.status !== 'ok') {
26
+ // The client forwards its current query string on the fetch so Dynamic block
27
+ // resolution sees the same urlQuery as an initial HTML load.
28
+ const pageConfig = await getPageConfig(context, { pageId, urlQuery: c.req.query() });
29
+ if (!pageConfig) {
28
30
  context.logger.info({ event: 'api_page_not_found', pageId });
29
31
  return c.json({ pageConfig: null }, 404);
30
32
  }
31
33
  context.logger.info({ event: 'api_page_view', pageId });
32
- return c.json({ pageConfig: result.pageConfig });
34
+ return c.json({ pageConfig });
33
35
  }
34
36
 
35
37
  export default apiPageHandler;
@@ -16,10 +16,7 @@
16
16
 
17
17
  async function sessionMockHandler(c) {
18
18
  const context = c.get('lowdefyContext');
19
- if (!context.user) {
20
- return c.json(null);
21
- }
22
- return c.json({ session: { id: 'e2e' }, user: context.user });
19
+ return c.json(context.session ?? {});
23
20
  }
24
21
 
25
22
  export default sessionMockHandler;
@@ -1,126 +0,0 @@
1
- /*
2
- Copyright 2020-2026 Lowdefy, Inc
3
-
4
- Licensed under the Apache License, Version 2.0 (the "License");
5
- you may not use this file except in compliance with the License.
6
- You may obtain a copy of the License at
7
-
8
- http://www.apache.org/licenses/LICENSE-2.0
9
-
10
- Unless required by applicable law or agreed to in writing, software
11
- distributed under the License is distributed on an "AS IS" BASIS,
12
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- See the License for the specific language governing permissions and
14
- limitations under the License.
15
- */
16
-
17
- import { jest } from '@jest/globals';
18
- import { ConfigError, LowdefyInternalError } from '@lowdefy/errors';
19
-
20
- import createHandleError from './createHandleError.js';
21
-
22
- // This sink is the sole writer of `error.handled`. Two callers read the flag and
23
- // neither can set it for itself: runRoutine's catch guards on `!error.handled` to
24
- // avoid logging a nested error twice, and the browser's handleError skips the
25
- // round-trip to /api/client-error when it is set. runRoutine deliberately gave up
26
- // its own assignment on the strength of this function, so the tests below pin the
27
- // assignment and — more importantly — its position ahead of everything that can
28
- // throw.
29
-
30
- const keyMap = { key_1: { key: 'root.pages[0:home]', '~r': 'ref_1', '~l': 5 } };
31
- const refMap = { ref_1: { path: 'pages/home.yaml' } };
32
-
33
- function testContext({ logger, readConfigFile, configDirectory } = {}) {
34
- return {
35
- configDirectory,
36
- logger: logger ?? { error: jest.fn() },
37
- readConfigFile:
38
- readConfigFile ??
39
- jest.fn(async (path) => {
40
- if (path === 'keyMap.json') return keyMap;
41
- if (path === 'refMap.json') return refMap;
42
- return null;
43
- }),
44
- };
45
- }
46
-
47
- beforeEach(() => {
48
- console.error = jest.fn();
49
- });
50
-
51
- test('handleError marks the error handled and resolves its location', async () => {
52
- const context = testContext();
53
- const handleError = createHandleError({ context });
54
- const error = new ConfigError('Bad config.', { configKey: 'key_1' });
55
-
56
- await handleError(error);
57
-
58
- expect(error.handled).toBe(true);
59
- expect(error.source).toBe('pages/home.yaml:5');
60
- expect(error.config).toBe('root.pages[0:home]');
61
- expect(context.logger.error).toHaveBeenCalledWith(error);
62
- });
63
-
64
- test('handleError marks a LowdefyInternalError handled even though it never gets a source', async () => {
65
- // The Decision 7 case. Location resolution is deliberately skipped for an
66
- // internal error, so `source` stays undefined - which is why the client's
67
- // already-logged gate reads `handled` and not `source`. Keyed on source, this
68
- // error was POSTed back to /api/client-error and logged a second time.
69
- const context = testContext();
70
- const handleError = createHandleError({ context });
71
- const error = new LowdefyInternalError('Unexpected condition.');
72
-
73
- await handleError(error);
74
-
75
- expect(error.handled).toBe(true);
76
- expect(error.source).toBeUndefined();
77
- expect(context.readConfigFile).not.toHaveBeenCalled();
78
- expect(context.logger.error).toHaveBeenCalledWith(error);
79
- });
80
-
81
- test('handleError marks the error handled before it logs, so a throwing logger cannot skip the flag', async () => {
82
- const logger = {
83
- error: jest.fn(() => {
84
- throw new Error('Log drain unreachable.');
85
- }),
86
- };
87
- const context = testContext({ logger });
88
- const handleError = createHandleError({ context });
89
- const error = new ConfigError('Bad config.', { configKey: 'key_1' });
90
-
91
- await handleError(error);
92
-
93
- // The sink swallows its own failure - reaching it is what makes the error
94
- // already-logged, so the flag must survive the log call failing. Were it set
95
- // after the log, this error would cross the wire unflagged and be logged a
96
- // second time from the browser.
97
- expect(error.handled).toBe(true);
98
- expect(logger.error).toHaveBeenCalled();
99
- expect(console.error).toHaveBeenCalled();
100
- });
101
-
102
- test('handleError marks the error handled when location resolution rejects', async () => {
103
- const readConfigFile = jest.fn(async () => {
104
- throw new Error('keyMap.json unreadable.');
105
- });
106
- const context = testContext({ readConfigFile });
107
- const handleError = createHandleError({ context });
108
- const error = new ConfigError('Bad config.', { configKey: 'key_1' });
109
-
110
- await handleError(error);
111
-
112
- expect(error.handled).toBe(true);
113
- // ConfigError's constructor initialises source to null; resolution failing
114
- // leaves it there rather than assigning.
115
- expect(error.source).toBeNull();
116
- });
117
-
118
- test('handleError marks a plain Error handled', async () => {
119
- const context = testContext();
120
- const handleError = createHandleError({ context });
121
- const error = new Error('Something broke.');
122
-
123
- await handleError(error);
124
-
125
- expect(error.handled).toBe(true);
126
- });
@@ -1,27 +0,0 @@
1
- /*
2
- Copyright 2020-2026 Lowdefy, Inc
3
-
4
- Licensed under the Apache License, Version 2.0 (the "License");
5
- you may not use this file except in compliance with the License.
6
- You may obtain a copy of the License at
7
-
8
- http://www.apache.org/licenses/LICENSE-2.0
9
-
10
- Unless required by applicable law or agreed to in writing, software
11
- distributed under the License is distributed on an "AS IS" BASIS,
12
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- See the License for the specific language governing permissions and
14
- limitations under the License.
15
- */
16
-
17
- // The resolved caller for the client - the same resolveAuthentication output
18
- // the first page load embeds. The base session carries neither roles (read
19
- // live from the active member row) nor the merged attributes bag, so the
20
- // client re-syncs from here (UpdateSession) after a session change such as
21
- // SetActiveOrganization.
22
- async function userHandler(c) {
23
- const context = c.get('lowdefyContext');
24
- return c.json({ user: context.user ?? null });
25
- }
26
-
27
- export default userHandler;