@lowdefy/server 5.6.0 → 6.1.0

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.
Files changed (79) hide show
  1. package/{pages/_app.js → client/App.jsx} +26 -34
  2. package/client/Page.jsx +122 -0
  3. package/client/clientBuildImports.test.mjs +61 -0
  4. package/client/main.jsx +39 -0
  5. package/client/shouldReloadForBuild.js +48 -0
  6. package/client/shouldReloadForBuild.test.mjs +75 -0
  7. package/lib/build/app.js +8 -1
  8. package/lib/build/appMeta.js +8 -1
  9. package/lib/build/auth.js +8 -1
  10. package/lib/build/config.js +8 -1
  11. package/lib/build/i18n.js +8 -1
  12. package/lib/build/logger.js +8 -1
  13. package/lib/build/theme.js +8 -1
  14. package/lib/client/auth/{Auth.js → Auth.jsx} +10 -2
  15. package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +21 -10
  16. package/lib/client/sentry/captureSentryError.js +1 -1
  17. package/lib/client/sentry/initSentryClient.js +1 -1
  18. package/lib/client/sentry/setSentryUser.js +1 -1
  19. package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +7 -4
  20. package/{pages/api/auth/[...nextauth].js → lib/server/auth/session.js} +9 -15
  21. package/lib/server/auth/strategies.js +47 -0
  22. package/lib/server/log/createHandleError.js +10 -5
  23. package/lib/server/log/createHandleError.test.mjs +85 -0
  24. package/lib/server/log/createLogger.js +28 -1
  25. package/lib/server/log/createLogger.test.mjs +102 -0
  26. package/lib/server/log/logRequest.js +47 -45
  27. package/lib/server/log/maskCredentialKeys.js +37 -0
  28. package/lib/server/log/projectErrorForLog.js +55 -0
  29. package/lib/server/log/redactUrlQuery.js +75 -0
  30. package/lib/server/log/redactUrlQuery.test.mjs +73 -0
  31. package/lib/server/{auth/getServerSession.js → log/serializeErrorForLog.js} +5 -8
  32. package/lib/server/log/serializeErrorForLog.test.mjs +325 -0
  33. package/lib/server/scrubSecrets.js +21 -0
  34. package/lib/server/sentry/captureSentryError.js +13 -1
  35. package/lib/server/sentry/captureSentryError.test.mjs +104 -0
  36. package/lib/server/sentry/initSentry.js +17 -4
  37. package/lib/server/sentry/initSentry.test.mjs +237 -0
  38. package/lib/server/sentry/scrubEvent.js +29 -0
  39. package/lib/server/sentry/scrubEvent.test.mjs +88 -0
  40. package/lib/server/sentry/setSentryUser.js +1 -1
  41. package/lowdefy/createCustomPluginTypesMap.mjs +1 -0
  42. package/package.json +54 -42
  43. package/package.original.json +56 -44
  44. package/src/app.js +140 -0
  45. package/src/html/getAssets.js +45 -0
  46. package/src/html/renderPage.js +80 -0
  47. package/src/html/template.js +95 -0
  48. package/src/index.js +95 -0
  49. package/src/lib/getPathSegments.js +25 -0
  50. package/src/lib/safeScriptJson.js +38 -0
  51. package/src/middleware/apiContext.js +138 -0
  52. package/src/middleware/apiContext.test.mjs +118 -0
  53. package/src/middleware/errorHandler.js +54 -0
  54. package/src/middleware/sentry.js +33 -0
  55. package/{pages/api/agent/[...path].js → src/routes/agent.js} +20 -42
  56. package/src/routes/apiPage.js +40 -0
  57. package/src/routes/apiPage.test.mjs +59 -0
  58. package/src/routes/auth.js +38 -0
  59. package/{pages/api/client-error.js → src/routes/clientError.js} +14 -16
  60. package/src/routes/cron.js +44 -0
  61. package/src/routes/cronForward.js +47 -0
  62. package/src/routes/detached.js +47 -0
  63. package/src/routes/endpoints.js +71 -0
  64. package/src/routes/mcp.js +34 -0
  65. package/{pages/api/request/[...path].js → src/routes/request.js} +10 -10
  66. package/src/routes/usage.js +46 -0
  67. package/src/routes/websocket.js +46 -0
  68. package/vite.config.js +61 -0
  69. package/.eslintrc.yaml +0 -1
  70. package/lib/client/Page.js +0 -56
  71. package/lib/server/apiWrapper.js +0 -87
  72. package/lib/server/serverSidePropsWrapper.js +0 -66
  73. package/next.config.js +0 -34
  74. package/pages/404.js +0 -49
  75. package/pages/[[...pageId]].js +0 -85
  76. package/pages/_document.js +0 -108
  77. package/pages/api/endpoints/[...endpointId].js +0 -32
  78. package/pages/api/usage.js +0 -67
  79. /package/{postcss.config.js → postcss.config.cjs} +0 -0
@@ -14,13 +14,7 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- // CSS layer order — MUST be the first CSS import. Next.js treats this as critical
18
- // CSS that loads before hydration, locking the cascade priority (antd > base/preflight)
19
- // before antd's StyleProvider injects @layer antd {} at runtime.
20
- import '../build/layer-order.css';
21
-
22
17
  import React, { useCallback, useRef } from 'react';
23
- import dynamic from 'next/dynamic';
24
18
 
25
19
  import { ErrorBoundary } from '@lowdefy/block-utils';
26
20
  import { useDarkMode, useLocale } from '@lowdefy/client';
@@ -28,23 +22,20 @@ import { StyleProvider } from '@ant-design/cssinjs';
28
22
  import { App as AntdApp, theme as antdTheme } from 'antd';
29
23
  import { XProvider } from '@ant-design/x';
30
24
 
25
+ import { serializer } from '@lowdefy/helpers';
26
+
31
27
  import antdLocaleLoaders from '../build/i18n/antdLocales.js';
32
28
  import antdXLocaleLoaders from '../build/i18n/antdXLocales.js';
33
29
  import dayjsLocaleMap from '../build/i18n/dayjsLocales.js';
34
- import Auth from '../lib/client/auth/Auth.js';
30
+ import rawLoggerConfig from '../build/logger.json';
31
+ import Auth from '../lib/client/auth/Auth.jsx';
35
32
  import createLogUsage from '../lib/client/createLogUsage.js';
36
33
  import initSentryClient from '../lib/client/sentry/initSentryClient.js';
37
- import loggerConfig from '../lib/build/logger.js';
38
34
  import setSentryUser from '../lib/client/sentry/setSentryUser.js';
35
+ import Page from './Page.jsx';
39
36
 
40
- // Must be in _app due to next specifications.
41
- import '../build/globals.css';
42
-
43
- // Initialize Sentry client once on module load
44
- initSentryClient({
45
- sentryDsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
46
- sentryConfig: loggerConfig.sentry,
47
- });
37
+ // Deserialize to restore arrays (e.g. sentry.userFields) from ~arr markers.
38
+ const loggerConfig = serializer.deserialize(rawLoggerConfig);
48
39
 
49
40
  function ThemeTokenResolver({ lowdefyRef, children }) {
50
41
  const { token } = antdTheme.useToken();
@@ -55,7 +46,16 @@ function ThemeTokenResolver({ lowdefyRef, children }) {
55
46
  return children;
56
47
  }
57
48
 
58
- function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
49
+ function App({ config }) {
50
+ const { rootConfig, session } = config;
51
+
52
+ // Sentry DSN arrives at runtime via the embedded config (the server reads
53
+ // SENTRY_DSN per process) — no build-time env inlining.
54
+ initSentryClient({
55
+ sentryDsn: config.sentryDsn,
56
+ sentryConfig: loggerConfig.sentry,
57
+ });
58
+
59
59
  const usageDataRef = useRef({});
60
60
  const lowdefyRef = useRef({ eventCallback: createLogUsage({ usageDataRef }) });
61
61
  if (rootConfig?.theme) {
@@ -67,7 +67,11 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
67
67
  configDarkMode: lowdefyRef.current.theme?.darkMode,
68
68
  });
69
69
 
70
- const { active: activeLocale, antdLocale, antdXLocale } = useLocale({
70
+ const {
71
+ active: activeLocale,
72
+ antdLocale,
73
+ antdXLocale,
74
+ } = useLocale({
71
75
  i18n: rootConfig?.i18n,
72
76
  antdLocaleLoaders,
73
77
  antdXLocaleLoaders,
@@ -98,9 +102,8 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
98
102
  // gives X components (Bubble, Sender, Conversations, ...) their built-in strings
99
103
  // alongside antd's. antd X ships only en_US + zh_CN; other locales fall back
100
104
  // to en_US for X-native strings.
101
- const mergedLocale = antdLocale || antdXLocale
102
- ? { ...(antdLocale ?? {}), ...(antdXLocale ?? {}) }
103
- : undefined;
105
+ const mergedLocale =
106
+ antdLocale || antdXLocale ? { ...(antdLocale ?? {}), ...(antdXLocale ?? {}) } : undefined;
104
107
 
105
108
  return (
106
109
  <StyleProvider layer>
@@ -131,14 +134,7 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
131
134
  user: auth.session,
132
135
  sentryConfig: loggerConfig.sentry,
133
136
  });
134
- return (
135
- <Component
136
- auth={auth}
137
- lowdefy={lowdefyRef.current}
138
- rootConfig={rootConfig}
139
- pageConfig={pageConfig}
140
- />
141
- );
137
+ return <Page auth={auth} config={config} lowdefy={lowdefyRef.current} />;
142
138
  }}
143
139
  </Auth>
144
140
  </ErrorBoundary>
@@ -149,8 +145,4 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
149
145
  );
150
146
  }
151
147
 
152
- const DynamicApp = dynamic(() => Promise.resolve(App), {
153
- ssr: false,
154
- });
155
-
156
- export default DynamicApp;
148
+ export default App;
@@ -0,0 +1,122 @@
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 React, { useEffect, useRef, useState } from 'react';
18
+
19
+ import Client from '@lowdefy/client';
20
+ import createRouter from '@lowdefy/client/adapters/createRouter.js';
21
+ import createLinkComponent from '@lowdefy/client/adapters/Link.js';
22
+ import { createUrl } from '@lowdefy/client/adapters/url.js';
23
+ import Head from '@lowdefy/client/adapters/Head.js';
24
+
25
+ import actions from '../build/plugins/actions.js';
26
+ import blockMetas from '../build/plugins/blockMetas.json';
27
+ import blocks from '../build/plugins/blocks.js';
28
+ import icons from '../build/plugins/icons.js';
29
+ import operators from '../build/plugins/operators/client.js';
30
+ import jsMap from '../build/plugins/operators/clientJsMap.js';
31
+ import appMeta from '../build/appMeta.json';
32
+
33
+ import shouldReloadForBuild from './shouldReloadForBuild.js';
34
+
35
+ // Replaces lib/client/Page.js. The first page renders from the config
36
+ // embedded in the HTML; SPA navigations fetch /api/page/* and swap pageConfig.
37
+ function Page({ auth, config, lowdefy }) {
38
+ const [pageConfig, setPageConfig] = useState(config.pageConfig);
39
+
40
+ const routerRef = useRef(null);
41
+ if (!routerRef.current) {
42
+ const router = createRouter({ basePath: config.basePath ?? '', window });
43
+ routerRef.current = {
44
+ router,
45
+ Link: createLinkComponent({ router }),
46
+ };
47
+ }
48
+ const { router, Link } = routerRef.current;
49
+
50
+ // Temporary sequence guard for the superseded-navigation race: a slow
51
+ // response for an abandoned navigation must not paint over a newer one.
52
+ // Remove when the loader-based lifecycle replaces these fetch paths.
53
+ const latestNavRef = useRef(0);
54
+
55
+ useEffect(() => {
56
+ const unsubscribe = router.subscribe(async ({ pageId }) => {
57
+ const token = ++latestNavRef.current;
58
+ const targetPageId = pageId ?? config.rootConfig.home.pageId;
59
+ try {
60
+ // Forward the current query string so server-side Dynamic block
61
+ // resolution sees the same urlQuery as an initial HTML load.
62
+ const res = await fetch(
63
+ `${router.basePath}/api/page/${targetPageId}${window.location.search}`
64
+ );
65
+ if (!res.ok) {
66
+ if (token !== latestNavRef.current) return;
67
+ if (targetPageId !== '404') {
68
+ router.replace({ pathname: '/404' });
69
+ }
70
+ return;
71
+ }
72
+ const { buildId, pageConfig: nextPageConfig } = await res.json();
73
+ if (token !== latestNavRef.current) return;
74
+ if (
75
+ shouldReloadForBuild({ bundleBuildId: appMeta.buildId, serverBuildId: buildId, window })
76
+ ) {
77
+ // The app was redeployed after this bundle loaded, so the config may
78
+ // reference _js functions and plugins this bundle does not carry.
79
+ // The router has already pushed the target URL, so a reload lands
80
+ // on the requested page with the current bundle.
81
+ window.location.reload();
82
+ return;
83
+ }
84
+ setPageConfig(nextPageConfig);
85
+ } catch (error) {
86
+ // Network failure on SPA navigation — fall back to a full page load.
87
+ if (token !== latestNavRef.current) return;
88
+ window.location.assign(
89
+ createUrl({
90
+ basePath: router.basePath,
91
+ pathname: targetPageId === config.rootConfig.home.pageId ? '/' : `/${targetPageId}`,
92
+ })
93
+ );
94
+ }
95
+ });
96
+ return unsubscribe;
97
+ }, []);
98
+
99
+ return (
100
+ <Client
101
+ auth={auth}
102
+ Components={{ Head, Link }}
103
+ config={{
104
+ pageConfig,
105
+ rootConfig: config.rootConfig,
106
+ }}
107
+ jsMap={jsMap}
108
+ lowdefy={lowdefy}
109
+ router={router}
110
+ types={{
111
+ actions,
112
+ blockMetas,
113
+ blocks,
114
+ icons,
115
+ operators,
116
+ }}
117
+ window={window}
118
+ />
119
+ );
120
+ }
121
+
122
+ export default Page;
@@ -0,0 +1,61 @@
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 fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ // Everything the client imports from build/ ships in the browser bundle. build/config.json is
22
+ // server-only: it carries every deployment environment's settings (other environments' urls,
23
+ // secret names, email filters). The client learns only the current environment's name and
24
+ // switched-off features, through appMeta. This allowlist fails the build of any new client import
25
+ // of a build artifact until it has been checked for what it exposes.
26
+ const allowed = new Set([
27
+ 'auth.json',
28
+ 'appMeta.json',
29
+ 'logger.json',
30
+ 'i18n/antdLocales.js',
31
+ 'i18n/antdXLocales.js',
32
+ 'i18n/dayjsLocales.js',
33
+ 'plugins/actions.js',
34
+ 'plugins/blockMetas.json',
35
+ 'plugins/blocks.js',
36
+ 'plugins/icons.js',
37
+ 'plugins/operators/client.js',
38
+ 'plugins/operators/clientJsMap.js',
39
+ ]);
40
+
41
+ const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
42
+
43
+ function listSourceFiles(dir) {
44
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
45
+ const full = path.join(dir, entry.name);
46
+ if (entry.isDirectory()) return listSourceFiles(full);
47
+ return /\.(js|jsx|mjs)$/.test(entry.name) && !entry.name.includes('.test.') ? [full] : [];
48
+ });
49
+ }
50
+
51
+ test('client code imports only client-safe build artifacts', () => {
52
+ const imported = ['client', 'lib/client'].flatMap((dir) =>
53
+ listSourceFiles(path.join(root, dir)).flatMap((file) =>
54
+ [...fs.readFileSync(file, 'utf8').matchAll(/from '[^']*\/build\/([^']+)'/g)].map(
55
+ (match) => ({ file: path.relative(root, file), artifact: match[1] })
56
+ )
57
+ )
58
+ );
59
+ expect(imported.length).toBeGreaterThan(0);
60
+ expect(imported.filter(({ artifact }) => !allowed.has(artifact))).toEqual([]);
61
+ });
@@ -0,0 +1,39 @@
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
+ // CSS layer order — MUST be the first CSS import. This locks the cascade
18
+ // priority (antd > base/preflight) before antd's StyleProvider injects
19
+ // @layer antd {} at runtime.
20
+ import '../build/layer-order.css';
21
+
22
+ import React from 'react';
23
+ import { createRoot } from 'react-dom/client';
24
+
25
+ import App from './App.jsx';
26
+
27
+ import '../build/globals.css';
28
+
29
+ const config = JSON.parse(document.getElementById('__LOWDEFY_CONFIG__').textContent);
30
+ const container = document.getElementById('root');
31
+
32
+ // Keep one React root across Vite HMR updates — re-running createRoot on a
33
+ // container that already has a root is an error.
34
+ const root = import.meta.hot?.data.root ?? createRoot(container);
35
+ if (import.meta.hot) {
36
+ import.meta.hot.data.root = root;
37
+ }
38
+
39
+ root.render(<App config={config} />);
@@ -0,0 +1,48 @@
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 { type } from '@lowdefy/helpers';
18
+
19
+ // sessionStorage key holding the server build this tab last reloaded for.
20
+ const RELOADED_FOR_BUILD_KEY = 'lowdefy.reloadedForBuild';
21
+
22
+ // True when a page config fetched during SPA navigation came from a newer
23
+ // deploy than the bundle this tab is running, so the tab must do a full load
24
+ // to pick up the current bundle. Without this the new config can reference
25
+ // _js functions the old bundle never shipped, and the page throws mid-event.
26
+ //
27
+ // A tab reloads at most once per server build: if the freshly loaded bundle
28
+ // still disagrees with the server (a rolling deploy answering from mixed
29
+ // versions, a proxy caching old HTML), reloading again would loop forever.
30
+ // The old-config-on-old-bundle throw is the lesser failure in that case.
31
+ function shouldReloadForBuild({ bundleBuildId, serverBuildId, window }) {
32
+ if (type.isNone(serverBuildId) || serverBuildId === bundleBuildId) {
33
+ return false;
34
+ }
35
+ try {
36
+ if (window.sessionStorage.getItem(RELOADED_FOR_BUILD_KEY) === serverBuildId) {
37
+ return false;
38
+ }
39
+ window.sessionStorage.setItem(RELOADED_FOR_BUILD_KEY, serverBuildId);
40
+ } catch (error) {
41
+ // Browsers with storage disabled throw on sessionStorage access. Without
42
+ // the once-per-build record the loop guard cannot work, so do not reload.
43
+ return false;
44
+ }
45
+ return true;
46
+ }
47
+
48
+ export default shouldReloadForBuild;
@@ -0,0 +1,75 @@
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 shouldReloadForBuild from './shouldReloadForBuild.js';
18
+
19
+ function createWindow({ stored = {}, throws = false } = {}) {
20
+ return {
21
+ sessionStorage: {
22
+ getItem: (key) => {
23
+ if (throws) throw new Error('storage disabled');
24
+ return stored[key] ?? null;
25
+ },
26
+ setItem: (key, value) => {
27
+ if (throws) throw new Error('storage disabled');
28
+ stored[key] = value;
29
+ },
30
+ },
31
+ };
32
+ }
33
+
34
+ test('shouldReloadForBuild is false when the server build matches the bundle', () => {
35
+ const window = createWindow();
36
+ expect(shouldReloadForBuild({ bundleBuildId: 'build-1', serverBuildId: 'build-1', window })).toBe(
37
+ false
38
+ );
39
+ });
40
+
41
+ test('shouldReloadForBuild is false when the server sends no build id', () => {
42
+ const window = createWindow();
43
+ expect(shouldReloadForBuild({ bundleBuildId: 'build-1', serverBuildId: undefined, window })).toBe(
44
+ false
45
+ );
46
+ expect(shouldReloadForBuild({ bundleBuildId: 'build-1', serverBuildId: null, window })).toBe(
47
+ false
48
+ );
49
+ });
50
+
51
+ test('shouldReloadForBuild is true once per newer server build, then false', () => {
52
+ const stored = {};
53
+ const window = createWindow({ stored });
54
+ expect(shouldReloadForBuild({ bundleBuildId: 'build-1', serverBuildId: 'build-2', window })).toBe(
55
+ true
56
+ );
57
+ expect(stored['lowdefy.reloadedForBuild']).toEqual('build-2');
58
+ expect(shouldReloadForBuild({ bundleBuildId: 'build-1', serverBuildId: 'build-2', window })).toBe(
59
+ false
60
+ );
61
+ });
62
+
63
+ test('shouldReloadForBuild reloads again when the server moves to a third build', () => {
64
+ const window = createWindow({ stored: { 'lowdefy.reloadedForBuild': 'build-2' } });
65
+ expect(shouldReloadForBuild({ bundleBuildId: 'build-2', serverBuildId: 'build-3', window })).toBe(
66
+ true
67
+ );
68
+ });
69
+
70
+ test('shouldReloadForBuild is false when sessionStorage is unavailable', () => {
71
+ const window = createWindow({ throws: true });
72
+ expect(shouldReloadForBuild({ bundleBuildId: 'build-1', serverBuildId: 'build-2', window })).toBe(
73
+ false
74
+ );
75
+ });
package/lib/build/app.js CHANGED
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/app.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/app.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/appMeta.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/appMeta.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
package/lib/build/auth.js CHANGED
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/auth.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/auth.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/config.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/config.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
package/lib/build/i18n.js CHANGED
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/i18n.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/i18n.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/logger.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/logger.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/theme.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/theme.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -17,10 +17,18 @@
17
17
  /* eslint-disable react/jsx-props-no-spreading */
18
18
 
19
19
  import React from 'react';
20
- import AuthConfigured from './AuthConfigured.js';
20
+ import AuthConfigured from './AuthConfigured.jsx';
21
21
  import AuthNotConfigured from './AuthNotConfigured.js';
22
22
 
23
- import authConfig from '../../build/auth.js';
23
+ import { serializer } from '@lowdefy/helpers';
24
+
25
+ // Client code imports the build JSON directly — Vite handles JSON imports;
26
+ // the lib/build/*.js wrappers are server-only (they read from disk).
27
+ // Deserialize to restore arrays from their ~arr build markers (providers
28
+ // must be a real array for single-provider inference in createAuthMethods).
29
+ import rawAuthConfig from '../../../build/auth.json';
30
+
31
+ const authConfig = serializer.deserialize(rawAuthConfig);
24
32
 
25
33
  function Auth({ children, session }) {
26
34
  if (authConfig.configured === true) {
@@ -17,9 +17,25 @@
17
17
  /* eslint-disable react/jsx-props-no-spreading */
18
18
 
19
19
  import React, { useEffect, useRef } from 'react';
20
- import { getSession, SessionProvider, signIn, signOut, useSession } from 'next-auth/react';
20
+ import {
21
+ authConfigManager,
22
+ getSession,
23
+ SessionProvider,
24
+ signIn,
25
+ signOut,
26
+ useSession,
27
+ } from '@hono/auth-js/react';
21
28
 
22
- import lowdefyConfig from '../../build/config.js';
29
+ // The app basePath, from Vite's BASE_URL (`${config.basePath}/`, set in
30
+ // vite.config.js). build/config.json is server-only - it carries every
31
+ // deployment environment's settings - so it is never imported into the client.
32
+ const basePath = import.meta.env.BASE_URL.replace(/\/$/, '');
33
+
34
+ // @hono/auth-js/react configures its fetch paths through a module-level
35
+ // manager instead of SessionProvider props.
36
+ if (basePath) {
37
+ authConfigManager.setConfig({ basePath: `${basePath}/api/auth` });
38
+ }
23
39
 
24
40
  function Session({ children }) {
25
41
  const wasAuthenticated = useRef(false);
@@ -32,9 +48,8 @@ function Session({ children }) {
32
48
  }
33
49
  }, [status]);
34
50
 
35
- // If session is passed to SessionProvider from getServerSideProps
36
- // we won't have a loading state here.
37
- // But 404 uses getStaticProps so we have this for 404.
51
+ // If session is passed to SessionProvider from the server-rendered config
52
+ // we won't have a loading state here, but unauthenticated first loads do.
38
53
  if (status === 'loading') {
39
54
  return '';
40
55
  }
@@ -43,12 +58,8 @@ function Session({ children }) {
43
58
 
44
59
  function AuthConfigured({ authConfig, children, serverSession }) {
45
60
  const auth = { authConfig, getSession, signIn, signOut };
46
- let basePath = lowdefyConfig.basePath;
47
- if (basePath) {
48
- basePath = `${basePath}/api/auth`;
49
- }
50
61
  return (
51
- <SessionProvider session={serverSession} basePath={basePath}>
62
+ <SessionProvider session={serverSession}>
52
63
  <Session>
53
64
  {(session) => {
54
65
  auth.session = session;
@@ -14,7 +14,7 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import * as Sentry from '@sentry/nextjs';
17
+ import * as Sentry from '@sentry/browser';
18
18
 
19
19
  function captureSentryError({ error, pageId, blockId, configLocation }) {
20
20
  const tags = {};