@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/server",
3
- "version": "5.6.0",
3
+ "version": "6.1.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "",
6
6
  "homepage": "https://lowdefy.com",
@@ -28,69 +28,81 @@
28
28
  "files": [
29
29
  "lib/*",
30
30
  "lowdefy/*",
31
- "pages/*",
32
31
  "public_default/*",
33
- "next.config.js",
34
- "postcss.config.js",
35
32
  "package.original.json",
36
33
  ".eslintrc.yaml",
37
- ".npmrc"
34
+ ".npmrc",
35
+ "src/*",
36
+ "client/*",
37
+ "vite.config.js",
38
+ "postcss.config.cjs"
38
39
  ],
39
40
  "scripts": {
40
41
  "build": "cp package.json package.original.json || copy package.json package.original.json",
42
+ "build:client": "vite build",
41
43
  "build:lowdefy": "node lowdefy/build.mjs",
42
- "build:next": "next build",
43
- "dev": "next dev",
44
- "start": "next start",
45
- "lint": "next lint",
46
- "next": "next",
47
- "prepublishOnly": "pnpm build"
44
+ "prepublishOnly": "pnpm build",
45
+ "start": "node src/index.js",
46
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
48
47
  },
49
48
  "dependencies": {
50
49
  "@ant-design/cssinjs": "2.1.2",
51
50
  "@ant-design/x": "2.7.0",
52
- "@lowdefy/actions-core": "5.6.0",
53
- "@lowdefy/api": "5.6.0",
54
- "@lowdefy/block-utils": "5.6.0",
55
- "@lowdefy/blocks-antd": "5.6.0",
56
- "@lowdefy/blocks-antd-x": "5.6.0",
57
- "@lowdefy/blocks-basic": "5.6.0",
58
- "@lowdefy/blocks-loaders": "5.6.0",
59
- "@lowdefy/blocks-tiptap": "5.6.0",
60
- "@lowdefy/client": "5.6.0",
61
- "@lowdefy/errors": "5.6.0",
62
- "@lowdefy/helpers": "5.6.0",
63
- "@lowdefy/layout": "5.6.0",
64
- "@lowdefy/logger": "5.6.0",
65
- "@lowdefy/node-utils": "5.6.0",
66
- "@lowdefy/operators-js": "5.6.0",
67
- "@lowdefy/plugin-next-auth": "5.6.0",
68
- "@sentry/nextjs": "8.53.0",
69
- "@tailwindcss/postcss": "4.2.1",
51
+ "@auth/core": "0.41.3",
52
+ "@hono/auth-js": "1.1.1",
53
+ "@hono/mcp": "0.3.0",
54
+ "@hono/node-server": "2.0.10",
55
+ "@lowdefy/actions-core": "6.1.0",
56
+ "@lowdefy/api": "6.1.0",
57
+ "@lowdefy/block-utils": "6.1.0",
58
+ "@lowdefy/blocks-aggrid": "6.1.0",
59
+ "@lowdefy/blocks-antd": "6.1.0",
60
+ "@lowdefy/blocks-antd-x": "6.1.0",
61
+ "@lowdefy/blocks-basic": "6.1.0",
62
+ "@lowdefy/blocks-echarts": "6.1.0",
63
+ "@lowdefy/blocks-files": "6.1.0",
64
+ "@lowdefy/blocks-loaders": "6.1.0",
65
+ "@lowdefy/blocks-markdown": "6.1.0",
66
+ "@lowdefy/blocks-tiptap": "6.1.0",
67
+ "@lowdefy/client": "6.1.0",
68
+ "@lowdefy/errors": "6.1.0",
69
+ "@lowdefy/helpers": "6.1.0",
70
+ "@lowdefy/layout": "6.1.0",
71
+ "@lowdefy/logger": "6.1.0",
72
+ "@lowdefy/node-utils": "6.1.0",
73
+ "@lowdefy/operators-js": "6.1.0",
74
+ "@lowdefy/plugin-next-auth": "6.1.0",
75
+ "@lowdefy/websockets-core": "6.1.0",
76
+ "@sentry/browser": "10.57.0",
77
+ "@sentry/node": "10.57.0",
78
+ "@sentry/vite-plugin": "5.3.0",
79
+ "@tailwindcss/postcss": "4.3.1",
80
+ "@vitejs/plugin-react": "6.0.2",
70
81
  "antd": "6.3.1",
71
- "dayjs": "1.11.19",
72
- "next": "16.1.6",
73
- "next-auth": "4.24.10",
74
- "pino": "8.16.2",
82
+ "dayjs": "1.11.20",
83
+ "hono": "4.13.5",
84
+ "pino": "10.3.1",
75
85
  "react": "18.2.0",
76
86
  "react-dom": "18.2.0",
77
87
  "react-icons": "5.6.0",
78
- "tailwindcss": "4.2.1",
79
- "uuid": "13.0.0"
88
+ "tailwindcss": "4.3.1",
89
+ "uuid": "14.0.0",
90
+ "vite": "8.0.16",
91
+ "ws": "8.21.0"
80
92
  },
81
93
  "devDependencies": {
82
- "@lowdefy/build": "5.6.0",
83
- "@next/eslint-plugin-next": "16.1.6",
84
- "@tailwindcss/postcss": "4.2.1",
85
- "tailwindcss": "4.2.1",
86
- "webpack": "5.94.0",
87
- "yaml": "2.3.4",
88
- "yargs": "17.7.2"
94
+ "@lowdefy/build": "6.1.0",
95
+ "@tailwindcss/postcss": "4.3.1",
96
+ "jest": "28.1.3",
97
+ "tailwindcss": "4.3.1",
98
+ "yaml": "2.9.0",
99
+ "yargs": "18.0.0"
89
100
  },
90
101
  "engines": {
91
- "node": ">=18"
102
+ "node": ">=22"
92
103
  },
93
104
  "publishConfig": {
94
105
  "access": "public"
95
- }
106
+ },
107
+ "type": "module"
96
108
  }
package/src/app.js ADDED
@@ -0,0 +1,140 @@
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 { Hono } from 'hono';
18
+ import { bodyLimit } from 'hono/body-limit';
19
+ import { compress } from 'hono/compress';
20
+ import { timeout } from 'hono/timeout';
21
+ import { initAuthConfig } from '@hono/auth-js';
22
+ import { serveStatic } from '@hono/node-server/serve-static';
23
+
24
+ import agentHandler from './routes/agent.js';
25
+ import apiContext from './middleware/apiContext.js';
26
+ import apiPageHandler from './routes/apiPage.js';
27
+ import authJson from '../lib/build/auth.js';
28
+ import authMiddleware from './routes/auth.js';
29
+ import clientErrorHandler from './routes/clientError.js';
30
+ import createErrorHandler from './middleware/errorHandler.js';
31
+ import createLogger from '../lib/server/log/createLogger.js';
32
+ import cronForwardHandler from './routes/cronForward.js';
33
+ import cronHandler from './routes/cron.js';
34
+ import detachedHandler from './routes/detached.js';
35
+ import endpointsHandler from './routes/endpoints.js';
36
+ import getAuthConfig from '../lib/server/auth/getAuthConfig.js';
37
+ import lowdefyConfig from '../lib/build/config.js';
38
+ import mcpHandler from './routes/mcp.js';
39
+ import renderPage from './html/renderPage.js';
40
+ import requestHandler from './routes/request.js';
41
+ import sentryMiddleware from './middleware/sentry.js';
42
+ import usageHandler from './routes/usage.js';
43
+ import websocketHandler from './routes/websocket.js';
44
+
45
+ const basePath = lowdefyConfig.basePath ?? '';
46
+
47
+ // Cap how long a single request may run. A request that hangs on an upstream call (DB, SMTP, an
48
+ // external API) would otherwise run to the platform's function limit — on serverless (e.g. Vercel
49
+ // Fluid) that is billed compute. Configured via `config.requestTimeout` (ms) in lowdefy.yaml;
50
+ // defaults to 30s, 0 disables. Agent streaming is exempt below since it is long-lived by design.
51
+ const requestTimeoutMs = lowdefyConfig.requestTimeout ?? 30000;
52
+
53
+ // `serveStaticAssets` is true for the Node server (it serves `dist/client` itself). On a
54
+ // platform that serves the built client + public files from a CDN (e.g. Vercel), pass false so
55
+ // the request handler skips the Node-only static middleware and only owns the dynamic routes.
56
+ function createApp({ serveStaticAssets = true } = {}) {
57
+ const app = basePath ? new Hono().basePath(basePath) : new Hono();
58
+ const logger = createLogger({ server: 'lowdefy' });
59
+
60
+ // Liveness endpoint for container health checks and orchestrator probes. Registered
61
+ // before all middleware so probes skip auth, session, request logging, and Sentry.
62
+ app.get('/api/lowdefy-health', (c) => c.json({ status: 'ok' }));
63
+
64
+ app.use('*', sentryMiddleware());
65
+
66
+ // Bound request duration so a hung upstream returns an error instead of running to the platform's
67
+ // function limit (cost protection on serverless). Agent streaming is long-lived by design, so it
68
+ // is exempt. Configured via `config.requestTimeout` in lowdefy.yaml (0 disables).
69
+ if (requestTimeoutMs > 0) {
70
+ app.use('*', async (c, next) => {
71
+ if (
72
+ c.req.path.includes('/api/agent/') ||
73
+ c.req.path.includes('/api/mcp') ||
74
+ c.req.path.includes('/api/websocket')
75
+ ) {
76
+ return next();
77
+ }
78
+ return timeout(requestTimeoutMs)(c, next);
79
+ });
80
+ }
81
+
82
+ // Next.js compressed responses by default — keep parity, but leave
83
+ // streaming responses (agent SSE) and websocket upgrades uncompressed.
84
+ app.use('*', async (c, next) => {
85
+ if (
86
+ c.req.path.includes('/api/agent/') ||
87
+ c.req.path.includes('/api/mcp') ||
88
+ c.req.path.includes('/api/websocket')
89
+ ) {
90
+ return next();
91
+ }
92
+ return compress()(c, next);
93
+ });
94
+
95
+ if (authJson.configured === true) {
96
+ app.use(
97
+ '*',
98
+ initAuthConfig(() => getAuthConfig({ logger }))
99
+ );
100
+ }
101
+
102
+ app.use('/api/*', apiContext());
103
+ app.use('/api/auth/*', authMiddleware());
104
+ app.all('/api/request/*', requestHandler);
105
+ // Endpoint payloads may carry base64 file content (emitFileContent + CallAPI);
106
+ // cap bodies at 10 MiB to match the agent route.
107
+ app.all('/api/endpoints/*', bodyLimit({ maxSize: 10 * 1024 * 1024 }), endpointsHandler);
108
+ app.get('/api/cron/*', cronHandler);
109
+ app.get('/api/cron-forward/*', cronForwardHandler);
110
+ app.post('/api/detached/*', detachedHandler);
111
+ app.all('/api/client-error', clientErrorHandler);
112
+ app.all('/api/usage', usageHandler);
113
+ app.all('/api/agent/*', bodyLimit({ maxSize: 10 * 1024 * 1024 }), agentHandler);
114
+ app.all('/api/mcp', bodyLimit({ maxSize: 10 * 1024 * 1024 }), mcpHandler);
115
+ app.get('/api/websocket', websocketHandler);
116
+ app.get('/api/page/*', apiPageHandler);
117
+
118
+ // Vite build output (includes public/ via Vite's publicDir copy). Falls
119
+ // through to the page routes when no file matches.
120
+ if (serveStaticAssets) {
121
+ app.use(
122
+ '/*',
123
+ serveStatic({
124
+ root: './dist/client',
125
+ rewriteRequestPath: basePath ? (path) => path.replace(basePath, '') : undefined,
126
+ })
127
+ );
128
+ }
129
+
130
+ app.use('/*', apiContext());
131
+ app.get('/', (c) => renderPage(c, { pageId: '' }));
132
+ app.get('/404', (c) => renderPage(c, { pageId: '404', status: 404 }));
133
+ app.get('/:rest{.+}', (c) => renderPage(c, { pageId: c.req.param('rest') }));
134
+
135
+ app.onError(createErrorHandler({ basePath, logger }));
136
+
137
+ return app;
138
+ }
139
+
140
+ export default createApp;
@@ -0,0 +1,45 @@
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
+
20
+ // The Vite manifest is read once at startup — deploys start a new server
21
+ // process after `vite build` completes, so assets never change at runtime.
22
+ // In-place deploys must build before restarting (documented).
23
+ let assets = null;
24
+
25
+ function getAssets() {
26
+ if (assets) {
27
+ return assets;
28
+ }
29
+ const manifestPath = path.join(process.cwd(), 'dist/client/.vite/manifest.json');
30
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
31
+ const entry = manifest['client/main.jsx'];
32
+ if (!entry) {
33
+ throw new Error(
34
+ 'Vite manifest has no "client/main.jsx" entry. Run the client build before starting the server.'
35
+ );
36
+ }
37
+ assets = {
38
+ js: entry.file,
39
+ css: entry.css ?? [],
40
+ imports: (entry.imports ?? []).map((key) => manifest[key]?.file).filter(Boolean),
41
+ };
42
+ return assets;
43
+ }
44
+
45
+ export default getAssets;
@@ -0,0 +1,80 @@
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 { getPageConfig, getRootConfig } from '@lowdefy/api';
18
+
19
+ import appJson from '../../lib/build/app.js';
20
+ import lowdefyConfig from '../../lib/build/config.js';
21
+ import themeConfig from '../../lib/build/theme.js';
22
+ import getAssets from './getAssets.js';
23
+ import template from './template.js';
24
+
25
+ const basePath = lowdefyConfig.basePath ?? '';
26
+
27
+ // Replaces pages/[[...pageId]].js getServerSideProps and pages/404.js
28
+ // getStaticProps. The home redirect logic lives here, not at the route level.
29
+ async function renderPage(c, { pageId, status = 200 }) {
30
+ const context = c.get('lowdefyContext');
31
+ const { logger, session } = context;
32
+
33
+ let resolvedPageId = pageId;
34
+ const rootConfig = await getRootConfig(context);
35
+
36
+ if (!resolvedPageId) {
37
+ const { home } = rootConfig;
38
+ if (home.configured === false) {
39
+ logger.info({ event: 'redirect_to_homepage', pageId: home.pageId });
40
+ return c.redirect(`${basePath}/${home.pageId}`, 302);
41
+ }
42
+ resolvedPageId = home.pageId;
43
+ }
44
+
45
+ const pageConfig = await getPageConfig(context, {
46
+ pageId: resolvedPageId,
47
+ urlQuery: c.req.query(),
48
+ });
49
+
50
+ if (!pageConfig) {
51
+ if (resolvedPageId === '404') {
52
+ // No 404 page in the build — return a plain 404 rather than redirecting in a loop.
53
+ return c.text('Page not found.', 404);
54
+ }
55
+ logger.info({ event: 'redirect_page_not_found', pageId: resolvedPageId });
56
+ return c.redirect(`${basePath}/404`, 302);
57
+ }
58
+
59
+ logger.info({ event: 'page_view', pageId: resolvedPageId });
60
+
61
+ const html = template({
62
+ appendBody: appJson.html?.appendBody ?? '',
63
+ appendHead: appJson.html?.appendHead ?? '',
64
+ assets: getAssets(),
65
+ basePath,
66
+ config: {
67
+ basePath,
68
+ pageConfig,
69
+ rootConfig,
70
+ sentryDsn: process.env.SENTRY_DSN ?? null,
71
+ session: session ?? null,
72
+ },
73
+ themeConfig,
74
+ title: pageConfig.properties?.title ?? resolvedPageId,
75
+ });
76
+
77
+ return c.html(html, status);
78
+ }
79
+
80
+ export default renderPage;
@@ -0,0 +1,95 @@
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 safeScriptJson from '../lib/safeScriptJson.js';
18
+
19
+ const HTML_ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' };
20
+
21
+ function escapeHtml(value) {
22
+ return String(value).replace(/[&<>"]/g, (c) => HTML_ESCAPES[c]);
23
+ }
24
+
25
+ // Synchronous script that creates the @layer order declaration and keeps it as
26
+ // the first child of <head> via MutationObserver. antd's CSS-in-JS uses
27
+ // prependQueue to inject <style> tags at the top of <head>, which would
28
+ // otherwise make @layer antd the first (lowest priority) layer.
29
+ // MutationObserver fires before paint, so the browser never sees the wrong
30
+ // cascade order.
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
+
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.
37
+ function darkModeScript({ configColorMode, darkBg, lightBg }) {
38
+ return `(function(){var c=${safeScriptJson(configColorMode)};var db=${safeScriptJson(
39
+ darkBg
40
+ )};var lb=${safeScriptJson(
41
+ 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})();`;
43
+ }
44
+
45
+ function template({
46
+ appendBody = '',
47
+ appendHead = '',
48
+ assets,
49
+ basePath = '',
50
+ config,
51
+ themeConfig = {},
52
+ title,
53
+ }) {
54
+ const VALID_COLOR_MODES = ['system', 'light', 'dark'];
55
+ const configColorMode = VALID_COLOR_MODES.includes(themeConfig.darkMode)
56
+ ? themeConfig.darkMode
57
+ : 'system';
58
+ const darkBg = themeConfig?.antd?.darkToken?.colorBgLayout ?? '#000';
59
+ const lightBg = themeConfig?.antd?.lightToken?.colorBgLayout ?? '';
60
+
61
+ const cssLinks = assets.css
62
+ .map((file) => `<link rel="stylesheet" href="${basePath}/${file}" />`)
63
+ .join('\n ');
64
+ const modulePreloads = assets.imports
65
+ .map((file) => `<link rel="modulepreload" href="${basePath}/${file}" />`)
66
+ .join('\n ');
67
+
68
+ // appendHead and appendBody are intentionally raw HTML from app config
69
+ // (analytics snippets, font links) — they must not be escaped. The config
70
+ // JSON and pre-hydration interpolations go through safeScriptJson.
71
+ return `<!DOCTYPE html>
72
+ <html class="lowdefy">
73
+ <head>
74
+ <meta charset="utf-8" />
75
+ <script>${layerOrderScript}</script>
76
+ <script>${darkModeScript({ configColorMode, darkBg, lightBg })}</script>
77
+ <link rel="manifest" href="${basePath}/manifest.webmanifest" />
78
+ <link rel="icon" type="image/svg+xml" href="${basePath}/icon.svg" />
79
+ <link rel="apple-touch-icon" href="${basePath}/apple-touch-icon.png" />
80
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
81
+ ${title ? `<title>${escapeHtml(title)}</title>` : ''}
82
+ ${cssLinks}
83
+ ${modulePreloads}
84
+ ${appendHead}
85
+ </head>
86
+ <body>
87
+ <div id="root"></div>
88
+ <script id="__LOWDEFY_CONFIG__" type="application/json">${safeScriptJson(config)}</script>
89
+ ${appendBody}
90
+ <script type="module" src="${basePath}/${assets.js}"></script>
91
+ </body>
92
+ </html>`;
93
+ }
94
+
95
+ export default template;
package/src/index.js ADDED
@@ -0,0 +1,95 @@
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 { serve } from '@hono/node-server';
18
+ import * as Sentry from '@sentry/node';
19
+ import { WebSocketServer } from 'ws';
20
+
21
+ import checkEnvironmentGuards from '@lowdefy/node-utils/checkEnvironmentGuards.js';
22
+
23
+ import initSentryServer from '../lib/server/sentry/initSentry.js';
24
+
25
+ // Auth.js v5 reads AUTH_URL but not NEXTAUTH_URL — alias the v4 variable
26
+ // before any auth config loads so existing deployments keep working.
27
+ if (process.env.NEXTAUTH_URL && !process.env.AUTH_URL) {
28
+ process.env.AUTH_URL = process.env.NEXTAUTH_URL;
29
+ }
30
+
31
+ const sentryEnabled = initSentryServer();
32
+
33
+ // Import after Sentry init so instrumentation observes the module graph.
34
+ const { default: createApp } = await import('./app.js');
35
+ const { default: createLogger } = await import('../lib/server/log/createLogger.js');
36
+ const { default: appMeta } = await import('../lib/build/appMeta.js');
37
+ const { default: config } = await import('../lib/build/config.js');
38
+
39
+ // The build checked the current environment's guards, but an image built once can be started with
40
+ // different variables, so they are checked again before the server takes any traffic.
41
+ checkEnvironmentGuards({
42
+ name: config.environment,
43
+ guards: config.environments?.[config.environment]?.guards,
44
+ });
45
+
46
+ const app = createApp();
47
+ const logger = createLogger({ server: 'lowdefy' });
48
+ const port = Number(process.env.PORT ?? 3000);
49
+
50
+ if (sentryEnabled) {
51
+ logger.info('Sentry enabled: server');
52
+ }
53
+
54
+ // Handles /api/websocket upgrades via serve({ websocket }). 256 KiB max frame,
55
+ // aligned with Vercel's documented default for WebSocket functions.
56
+ const wss = new WebSocketServer({ noServer: true, maxPayload: 256 * 1024 });
57
+
58
+ const server = serve({ fetch: app.fetch, port, websocket: { server: wss } }, (info) => {
59
+ // lowdefy_version logs once here rather than on every line — it is implied
60
+ // by the build, unlike the per-line deploy identity fields on the logger.
61
+ logger.info(
62
+ { port: info.port, lowdefy_version: appMeta.lowdefyVersion },
63
+ `Lowdefy server listening on http://localhost:${info.port}`
64
+ );
65
+ });
66
+
67
+ // Container runtimes send SIGTERM and escalate to SIGKILL after a grace period
68
+ // (Docker defaults to 10s) — finish in-flight work and exit before that deadline.
69
+ let shuttingDown = false;
70
+ function shutdown() {
71
+ if (shuttingDown) {
72
+ return;
73
+ }
74
+ shuttingDown = true;
75
+ // 1001 "going away" — clients reconnect and resubscribe on their own.
76
+ for (const client of wss.clients) {
77
+ client.close(1001, 'Server shutting down');
78
+ }
79
+ // Drop idle keep-alive connections so close() only waits on in-flight requests.
80
+ server.closeIdleConnections?.();
81
+ server.close(() => {
82
+ // Flush queued Sentry events; resolves immediately when Sentry is not enabled.
83
+ Sentry.close(2000).finally(() => process.exit(0));
84
+ });
85
+ // In-flight requests that outlive the grace period would otherwise hold the
86
+ // process open until SIGKILL — exit first so shutdown stays clean.
87
+ setTimeout(() => process.exit(1), 8000).unref();
88
+ }
89
+
90
+ process.on('SIGINT', shutdown);
91
+ process.on('SIGTERM', shutdown);
92
+
93
+ // Vercel's Node.js builder consumes the exported server — the same pattern
94
+ // Vercel documents for WebSocket support with Hono on Fluid compute.
95
+ export default server;
@@ -0,0 +1,25 @@
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
+ // Splits the rest of a catch-all route into decoded segments, independent of
18
+ // any configured basePath. Mirrors the Next.js [...param] query arrays —
19
+ // nested ids (slashes in pageId or endpointId) are real.
20
+ function getPathSegments(c, prefix) {
21
+ const rest = c.req.path.split(prefix)[1] ?? '';
22
+ return rest.split('/').filter(Boolean).map(decodeURIComponent);
23
+ }
24
+
25
+ export default getPathSegments;
@@ -0,0 +1,38 @@
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
+ // Escape characters that could break out of the enclosing <script> tag or
18
+ // terminate a JS string literal. Used to defuse the js/bad-code-sanitization
19
+ // class of injection for values embedded into inline scripts and the
20
+ // application/json config script.
21
+ const SCRIPT_ESCAPES = {
22
+ '<': '\\u003C',
23
+ '>': '\\u003E',
24
+ '\b': '\\b',
25
+ '\f': '\\f',
26
+ '\n': '\\n',
27
+ '\r': '\\r',
28
+ '\t': '\\t',
29
+ '\0': '\\0',
30
+ '\u2028': '\\u2028',
31
+ '\u2029': '\\u2029',
32
+ };
33
+
34
+ function safeScriptJson(value) {
35
+ return JSON.stringify(value).replace(/[<>\b\f\n\r\t\0\u2028\u2029]/g, (c) => SCRIPT_ESCAPES[c]);
36
+ }
37
+
38
+ export default safeScriptJson;