@lowdefy/server 5.6.0 → 6.0.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 (61) hide show
  1. package/{pages/_app.js → client/App.jsx} +26 -34
  2. package/client/Page.jsx +109 -0
  3. package/client/main.jsx +39 -0
  4. package/lib/build/app.js +8 -1
  5. package/lib/build/appMeta.js +8 -1
  6. package/lib/build/auth.js +8 -1
  7. package/lib/build/config.js +8 -1
  8. package/lib/build/i18n.js +8 -1
  9. package/lib/build/logger.js +8 -1
  10. package/lib/build/theme.js +8 -1
  11. package/lib/client/auth/{Auth.js → Auth.jsx} +10 -2
  12. package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +22 -10
  13. package/lib/client/sentry/captureSentryError.js +1 -1
  14. package/lib/client/sentry/initSentryClient.js +1 -1
  15. package/lib/client/sentry/setSentryUser.js +1 -1
  16. package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +5 -4
  17. package/lib/server/auth/{getServerSession.js → session.js} +9 -6
  18. package/lib/server/auth/strategies.js +47 -0
  19. package/lib/server/log/createHandleError.js +1 -1
  20. package/lib/server/log/createLogger.js +18 -1
  21. package/lib/server/log/logRequest.js +41 -45
  22. package/lib/server/sentry/captureSentryError.js +1 -1
  23. package/lib/server/sentry/initSentry.js +6 -4
  24. package/lib/server/sentry/setSentryUser.js +1 -1
  25. package/lowdefy/createCustomPluginTypesMap.mjs +1 -0
  26. package/package.json +52 -39
  27. package/package.original.json +54 -41
  28. package/src/app.js +140 -0
  29. package/src/html/getAssets.js +45 -0
  30. package/src/html/renderPage.js +80 -0
  31. package/src/html/template.js +95 -0
  32. package/src/index.js +85 -0
  33. package/src/lib/getPathSegments.js +25 -0
  34. package/src/lib/safeScriptJson.js +38 -0
  35. package/src/middleware/apiContext.js +135 -0
  36. package/src/middleware/errorHandler.js +54 -0
  37. package/src/middleware/sentry.js +33 -0
  38. package/{pages/api/agent/[...path].js → src/routes/agent.js} +20 -42
  39. package/src/routes/apiPage.js +37 -0
  40. package/src/routes/auth.js +38 -0
  41. package/{pages/api/client-error.js → src/routes/clientError.js} +14 -16
  42. package/src/routes/cron.js +44 -0
  43. package/src/routes/cronForward.js +47 -0
  44. package/src/routes/detached.js +47 -0
  45. package/src/routes/endpoints.js +71 -0
  46. package/src/routes/mcp.js +34 -0
  47. package/{pages/api/request/[...path].js → src/routes/request.js} +10 -10
  48. package/src/routes/usage.js +46 -0
  49. package/src/routes/websocket.js +46 -0
  50. package/vite.config.js +61 -0
  51. package/.eslintrc.yaml +0 -1
  52. package/lib/client/Page.js +0 -56
  53. package/lib/server/serverSidePropsWrapper.js +0 -66
  54. package/next.config.js +0 -34
  55. package/pages/404.js +0 -49
  56. package/pages/[[...pageId]].js +0 -85
  57. package/pages/_document.js +0 -108
  58. package/pages/api/auth/[...nextauth].js +0 -37
  59. package/pages/api/endpoints/[...endpointId].js +0 -32
  60. package/pages/api/usage.js +0 -67
  61. /package/{postcss.config.js → postcss.config.cjs} +0 -0
@@ -14,53 +14,49 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- // TODO: Better name needed here maybe?
18
- function logRequest({ context }) {
17
+ // Emits one access-log line per request, after the response is finalized.
18
+ function logRequest({ context, status, durationMs }) {
19
19
  const { headers = {}, user = {} } = context;
20
- context.logger.info({
21
- // TODO:
22
- // app_name
23
- // app_version
24
- // lowdefy_version
25
- // build_hash
26
- // config_hash
27
- user: {
28
- id: user.id,
29
- roles: user.roles,
30
- sub: user.sub,
31
- session_id: user.session_id, // TODO: Implement session id
20
+ context.logger.info(
21
+ {
22
+ user: {
23
+ id: user.id,
24
+ roles: user.roles,
25
+ sub: user.sub,
26
+ session_id: user.session_id, // TODO: Implement session id
27
+ },
28
+ url: context.req.url,
29
+ method: context.req.method,
30
+ hostname: context.req.hostname,
31
+ status,
32
+ duration_ms: durationMs,
33
+ headers: {
34
+ 'accept-language': headers['accept-language'],
35
+ 'sec-ch-ua-mobile': headers['sec-ch-ua-mobile'],
36
+ 'sec-ch-ua-platform': headers['sec-ch-ua-platform'],
37
+ 'sec-ch-ua': headers['sec-ch-ua'],
38
+ 'user-agent': headers['user-agent'],
39
+ host: headers.host,
40
+ referer: headers.referer,
41
+ 'x-forwarded-for': headers['x-forwarded-for'],
42
+ // Vercel headers
43
+ 'x-vercel-id': headers['x-vercel-id'],
44
+ 'x-real-ip': headers['x-real-ip'],
45
+ 'x-vercel-ip-country': headers['x-vercel-ip-country'],
46
+ 'x-vercel-ip-country-region': headers['x-vercel-ip-country-region'],
47
+ 'x-vercel-ip-city': headers['x-vercel-ip-city'],
48
+ 'x-vercel-ip-latitude': headers['x-vercel-ip-latitude'],
49
+ 'x-vercel-ip-longitude': headers['x-vercel-ip-longitude'],
50
+ 'x-vercel-ip-timezone': headers['x-vercel-ip-timezone'],
51
+ // Cloudflare headers
52
+ 'cf-connecting-ip': headers['cf-connecting-ip'],
53
+ 'cf-ray': headers['cf-ray'],
54
+ 'cf-ipcountry': headers['cf-ipcountry'],
55
+ 'cf-visitor': headers['cf-visitor'],
56
+ },
32
57
  },
33
- url: context.req.url,
34
- method: context.req.method,
35
- resolvedUrl: context.nextContext?.resolvedUrl,
36
- hostname: context.req.hostname,
37
- headers: {
38
- 'accept-language': headers['accept-language'],
39
- 'sec-ch-ua-mobile': headers['sec-ch-ua-mobile'],
40
- 'sec-ch-ua-platform': headers['sec-ch-ua-platform'],
41
- 'sec-ch-ua': headers['sec-ch-ua'],
42
- 'user-agent': headers['user-agent'],
43
- host: headers.host,
44
- referer: headers.referer,
45
- 'x-forward-for': headers['x-forward-for'],
46
- // Vercel headers
47
- 'x-vercel-id': headers['x-vercel-id'],
48
- 'x-real-ip': headers['x-real-ip'],
49
- 'x-vercel-ip-country': headers['x-vercel-ip-country'],
50
- 'x-vercel-ip-country-region': headers['x-vercel-ip-country-region'],
51
- 'x-vercel-ip-city': headers['x-vercel-ip-city'],
52
- 'x-vercel-ip-latitude': headers['x-vercel-ip-latitude'],
53
- 'x-vercel-ip-longitude': headers['x-vercel-ip-longitude'],
54
- 'x-vercel-ip-timezone': headers['x-vercel-ip-timezone'],
55
- // Cloudflare headers
56
- 'cf-connecting-ip': headers['cf-connecting-ip'],
57
- 'cf-ray': headers['cf-ray'],
58
- 'cf-ipcountry': headers['cf-ipcountry'],
59
- 'cf-visitor': headers['cf-visitor'],
60
- },
61
- });
62
- // TODO:
63
- // Next local? nextContext.locale, nextContext.locales, nextContext.defaultLocale
58
+ 'request completed'
59
+ );
64
60
  }
65
61
 
66
62
  export default logRequest;
@@ -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/node';
18
18
 
19
19
  function captureSentryError({ error, context, configLocation }) {
20
20
  // No-op if Sentry not initialized (DSN not set)
@@ -14,21 +14,23 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import * as Sentry from '@sentry/nextjs';
17
+ import * as Sentry from '@sentry/node';
18
18
 
19
19
  import loggerConfig from '../../build/logger.js';
20
20
 
21
+ // Returns true when Sentry was initialized, so the caller can log it through
22
+ // the structured logger — this module runs before the logger exists.
21
23
  function initSentryServer() {
22
24
  // No-op if SENTRY_DSN not set
23
25
  if (!process.env.SENTRY_DSN) {
24
- return;
26
+ return false;
25
27
  }
26
28
 
27
29
  const sentryConfig = loggerConfig.sentry || {};
28
30
 
29
31
  // No-op if server logging is explicitly disabled
30
32
  if (sentryConfig.server === false) {
31
- return;
33
+ return false;
32
34
  }
33
35
 
34
36
  Sentry.init({
@@ -37,7 +39,7 @@ function initSentryServer() {
37
39
  tracesSampleRate: sentryConfig.tracesSampleRate ?? 0.1,
38
40
  });
39
41
 
40
- console.log('Sentry enabled: server');
42
+ return true;
41
43
  }
42
44
 
43
45
  export default initSentryServer;
@@ -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/node';
18
18
 
19
19
  function setSentryUser({ user, sentryConfig }) {
20
20
  // No-op if Sentry not initialized (DSN not set)
@@ -55,6 +55,7 @@ async function createCustomPluginTypesMap({ directories }) {
55
55
  server: {},
56
56
  },
57
57
  requests: {},
58
+ websockets: {},
58
59
  };
59
60
 
60
61
  const pluginDefinitions = await getPluginDefinitions({ directories });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/server",
3
- "version": "5.6.0",
3
+ "version": "6.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "",
6
6
  "homepage": "https://lowdefy.com",
@@ -28,68 +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
  "dependencies": {
40
41
  "@ant-design/cssinjs": "2.1.2",
41
42
  "@ant-design/x": "2.7.0",
42
- "@lowdefy/actions-core": "5.6.0",
43
- "@lowdefy/api": "5.6.0",
44
- "@lowdefy/block-utils": "5.6.0",
45
- "@lowdefy/blocks-antd": "5.6.0",
46
- "@lowdefy/blocks-antd-x": "5.6.0",
47
- "@lowdefy/blocks-basic": "5.6.0",
48
- "@lowdefy/blocks-loaders": "5.6.0",
49
- "@lowdefy/blocks-tiptap": "5.6.0",
50
- "@lowdefy/client": "5.6.0",
51
- "@lowdefy/errors": "5.6.0",
52
- "@lowdefy/helpers": "5.6.0",
53
- "@lowdefy/layout": "5.6.0",
54
- "@lowdefy/logger": "5.6.0",
55
- "@lowdefy/node-utils": "5.6.0",
56
- "@lowdefy/operators-js": "5.6.0",
57
- "@lowdefy/plugin-next-auth": "5.6.0",
43
+ "@auth/core": "0.41.2",
44
+ "@hono/auth-js": "1.1.1",
45
+ "@hono/mcp": "0.3.0",
46
+ "@hono/node-server": "2.0.4",
47
+ "@lowdefy/actions-core": "6.0.0",
48
+ "@lowdefy/api": "6.0.0",
49
+ "@lowdefy/block-utils": "6.0.0",
50
+ "@lowdefy/blocks-aggrid": "6.0.0",
51
+ "@lowdefy/blocks-antd": "6.0.0",
52
+ "@lowdefy/blocks-antd-x": "6.0.0",
53
+ "@lowdefy/blocks-basic": "6.0.0",
54
+ "@lowdefy/blocks-echarts": "6.0.0",
55
+ "@lowdefy/blocks-files": "6.0.0",
56
+ "@lowdefy/blocks-loaders": "6.0.0",
57
+ "@lowdefy/blocks-markdown": "6.0.0",
58
+ "@lowdefy/blocks-tiptap": "6.0.0",
59
+ "@lowdefy/client": "6.0.0",
60
+ "@lowdefy/errors": "6.0.0",
61
+ "@lowdefy/helpers": "6.0.0",
62
+ "@lowdefy/layout": "6.0.0",
63
+ "@lowdefy/logger": "6.0.0",
64
+ "@lowdefy/node-utils": "6.0.0",
65
+ "@lowdefy/operators-js": "6.0.0",
66
+ "@lowdefy/plugin-next-auth": "6.0.0",
67
+ "@lowdefy/websockets-core": "6.0.0",
68
+ "@sentry/browser": "10.57.0",
58
69
  "@sentry/nextjs": "8.53.0",
59
- "@tailwindcss/postcss": "4.2.1",
70
+ "@sentry/node": "10.57.0",
71
+ "@sentry/vite-plugin": "5.3.0",
72
+ "@tailwindcss/postcss": "4.3.1",
73
+ "@vitejs/plugin-react": "6.0.2",
60
74
  "antd": "6.3.1",
61
- "dayjs": "1.11.19",
62
- "next": "16.1.6",
63
- "next-auth": "4.24.10",
64
- "pino": "8.16.2",
75
+ "dayjs": "1.11.20",
76
+ "hono": "4.12.25",
77
+ "pino": "10.3.1",
65
78
  "react": "18.2.0",
66
79
  "react-dom": "18.2.0",
67
80
  "react-icons": "5.6.0",
68
- "tailwindcss": "4.2.1",
69
- "uuid": "13.0.0"
81
+ "tailwindcss": "4.3.1",
82
+ "uuid": "14.0.0",
83
+ "vite": "8.0.16",
84
+ "ws": "8.18.0"
70
85
  },
71
86
  "devDependencies": {
72
- "@lowdefy/build": "5.6.0",
87
+ "@lowdefy/build": "6.0.0",
73
88
  "@next/eslint-plugin-next": "16.1.6",
74
- "@tailwindcss/postcss": "4.2.1",
75
- "tailwindcss": "4.2.1",
89
+ "@tailwindcss/postcss": "4.3.1",
90
+ "tailwindcss": "4.3.1",
76
91
  "webpack": "5.94.0",
77
- "yaml": "2.3.4",
78
- "yargs": "17.7.2"
92
+ "yaml": "2.9.0",
93
+ "yargs": "18.0.0"
79
94
  },
80
95
  "engines": {
81
- "node": ">=18"
96
+ "node": ">=22"
82
97
  },
83
98
  "publishConfig": {
84
99
  "access": "public"
85
100
  },
101
+ "type": "module",
86
102
  "scripts": {
87
103
  "build": "cp package.json package.original.json || copy package.json package.original.json",
104
+ "build:client": "vite build",
88
105
  "build:lowdefy": "node lowdefy/build.mjs",
89
- "build:next": "next build",
90
- "dev": "next dev",
91
- "start": "next start",
92
- "lint": "next lint",
93
- "next": "next"
106
+ "start": "node src/index.js"
94
107
  }
95
108
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/server",
3
- "version": "5.6.0",
3
+ "version": "6.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "",
6
6
  "homepage": "https://lowdefy.com",
@@ -28,69 +28,82 @@
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"
48
46
  },
49
47
  "dependencies": {
50
48
  "@ant-design/cssinjs": "2.1.2",
51
49
  "@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",
50
+ "@auth/core": "0.41.2",
51
+ "@hono/auth-js": "1.1.1",
52
+ "@hono/mcp": "0.3.0",
53
+ "@hono/node-server": "2.0.4",
54
+ "@lowdefy/actions-core": "6.0.0",
55
+ "@lowdefy/api": "6.0.0",
56
+ "@lowdefy/block-utils": "6.0.0",
57
+ "@lowdefy/blocks-aggrid": "6.0.0",
58
+ "@lowdefy/blocks-antd": "6.0.0",
59
+ "@lowdefy/blocks-antd-x": "6.0.0",
60
+ "@lowdefy/blocks-basic": "6.0.0",
61
+ "@lowdefy/blocks-echarts": "6.0.0",
62
+ "@lowdefy/blocks-files": "6.0.0",
63
+ "@lowdefy/blocks-loaders": "6.0.0",
64
+ "@lowdefy/blocks-markdown": "6.0.0",
65
+ "@lowdefy/blocks-tiptap": "6.0.0",
66
+ "@lowdefy/client": "6.0.0",
67
+ "@lowdefy/errors": "6.0.0",
68
+ "@lowdefy/helpers": "6.0.0",
69
+ "@lowdefy/layout": "6.0.0",
70
+ "@lowdefy/logger": "6.0.0",
71
+ "@lowdefy/node-utils": "6.0.0",
72
+ "@lowdefy/operators-js": "6.0.0",
73
+ "@lowdefy/plugin-next-auth": "6.0.0",
74
+ "@lowdefy/websockets-core": "6.0.0",
75
+ "@sentry/browser": "10.57.0",
68
76
  "@sentry/nextjs": "8.53.0",
69
- "@tailwindcss/postcss": "4.2.1",
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.12.25",
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.18.0"
80
92
  },
81
93
  "devDependencies": {
82
- "@lowdefy/build": "5.6.0",
94
+ "@lowdefy/build": "6.0.0",
83
95
  "@next/eslint-plugin-next": "16.1.6",
84
- "@tailwindcss/postcss": "4.2.1",
85
- "tailwindcss": "4.2.1",
96
+ "@tailwindcss/postcss": "4.3.1",
97
+ "tailwindcss": "4.3.1",
86
98
  "webpack": "5.94.0",
87
- "yaml": "2.3.4",
88
- "yargs": "17.7.2"
99
+ "yaml": "2.9.0",
100
+ "yargs": "18.0.0"
89
101
  },
90
102
  "engines": {
91
- "node": ">=18"
103
+ "node": ">=22"
92
104
  },
93
105
  "publishConfig": {
94
106
  "access": "public"
95
- }
107
+ },
108
+ "type": "module"
96
109
  }
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;