@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.
- package/{pages/_app.js → client/App.jsx} +26 -34
- package/client/Page.jsx +122 -0
- package/client/clientBuildImports.test.mjs +61 -0
- package/client/main.jsx +39 -0
- package/client/shouldReloadForBuild.js +48 -0
- package/client/shouldReloadForBuild.test.mjs +75 -0
- package/lib/build/app.js +8 -1
- package/lib/build/appMeta.js +8 -1
- package/lib/build/auth.js +8 -1
- package/lib/build/config.js +8 -1
- package/lib/build/i18n.js +8 -1
- package/lib/build/logger.js +8 -1
- package/lib/build/theme.js +8 -1
- package/lib/client/auth/{Auth.js → Auth.jsx} +10 -2
- package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +21 -10
- package/lib/client/sentry/captureSentryError.js +1 -1
- package/lib/client/sentry/initSentryClient.js +1 -1
- package/lib/client/sentry/setSentryUser.js +1 -1
- package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +7 -4
- package/{pages/api/auth/[...nextauth].js → lib/server/auth/session.js} +9 -15
- package/lib/server/auth/strategies.js +47 -0
- package/lib/server/log/createHandleError.js +10 -5
- package/lib/server/log/createHandleError.test.mjs +85 -0
- package/lib/server/log/createLogger.js +28 -1
- package/lib/server/log/createLogger.test.mjs +102 -0
- package/lib/server/log/logRequest.js +47 -45
- package/lib/server/log/maskCredentialKeys.js +37 -0
- package/lib/server/log/projectErrorForLog.js +55 -0
- package/lib/server/log/redactUrlQuery.js +75 -0
- package/lib/server/log/redactUrlQuery.test.mjs +73 -0
- package/lib/server/{auth/getServerSession.js → log/serializeErrorForLog.js} +5 -8
- package/lib/server/log/serializeErrorForLog.test.mjs +325 -0
- package/lib/server/scrubSecrets.js +21 -0
- package/lib/server/sentry/captureSentryError.js +13 -1
- package/lib/server/sentry/captureSentryError.test.mjs +104 -0
- package/lib/server/sentry/initSentry.js +17 -4
- package/lib/server/sentry/initSentry.test.mjs +237 -0
- package/lib/server/sentry/scrubEvent.js +29 -0
- package/lib/server/sentry/scrubEvent.test.mjs +88 -0
- package/lib/server/sentry/setSentryUser.js +1 -1
- package/lowdefy/createCustomPluginTypesMap.mjs +1 -0
- package/package.json +54 -42
- package/package.original.json +56 -44
- package/src/app.js +140 -0
- package/src/html/getAssets.js +45 -0
- package/src/html/renderPage.js +80 -0
- package/src/html/template.js +95 -0
- package/src/index.js +95 -0
- package/src/lib/getPathSegments.js +25 -0
- package/src/lib/safeScriptJson.js +38 -0
- package/src/middleware/apiContext.js +138 -0
- package/src/middleware/apiContext.test.mjs +118 -0
- package/src/middleware/errorHandler.js +54 -0
- package/src/middleware/sentry.js +33 -0
- package/{pages/api/agent/[...path].js → src/routes/agent.js} +20 -42
- package/src/routes/apiPage.js +40 -0
- package/src/routes/apiPage.test.mjs +59 -0
- package/src/routes/auth.js +38 -0
- package/{pages/api/client-error.js → src/routes/clientError.js} +14 -16
- package/src/routes/cron.js +44 -0
- package/src/routes/cronForward.js +47 -0
- package/src/routes/detached.js +47 -0
- package/src/routes/endpoints.js +71 -0
- package/src/routes/mcp.js +34 -0
- package/{pages/api/request/[...path].js → src/routes/request.js} +10 -10
- package/src/routes/usage.js +46 -0
- package/src/routes/websocket.js +46 -0
- package/vite.config.js +61 -0
- package/.eslintrc.yaml +0 -1
- package/lib/client/Page.js +0 -56
- package/lib/server/apiWrapper.js +0 -87
- package/lib/server/serverSidePropsWrapper.js +0 -66
- package/next.config.js +0 -34
- package/pages/404.js +0 -49
- package/pages/[[...pageId]].js +0 -85
- package/pages/_document.js +0 -108
- package/pages/api/endpoints/[...endpointId].js +0 -32
- package/pages/api/usage.js +0 -67
- /package/{postcss.config.js → postcss.config.cjs} +0 -0
package/package.original.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/server",
|
|
3
|
-
"version": "
|
|
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
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
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
|
-
"@
|
|
53
|
-
"@
|
|
54
|
-
"@
|
|
55
|
-
"@
|
|
56
|
-
"@lowdefy/
|
|
57
|
-
"@lowdefy/
|
|
58
|
-
"@lowdefy/
|
|
59
|
-
"@lowdefy/blocks-
|
|
60
|
-
"@lowdefy/
|
|
61
|
-
"@lowdefy/
|
|
62
|
-
"@lowdefy/
|
|
63
|
-
"@lowdefy/
|
|
64
|
-
"@lowdefy/
|
|
65
|
-
"@lowdefy/
|
|
66
|
-
"@lowdefy/
|
|
67
|
-
"@lowdefy/
|
|
68
|
-
"@
|
|
69
|
-
"@
|
|
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.
|
|
72
|
-
"
|
|
73
|
-
"
|
|
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.
|
|
79
|
-
"uuid": "
|
|
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": "
|
|
83
|
-
"@
|
|
84
|
-
"
|
|
85
|
-
"tailwindcss": "4.
|
|
86
|
-
"
|
|
87
|
-
"
|
|
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": ">=
|
|
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 = { '&': '&', '<': '<', '>': '>', '"': '"' };
|
|
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;
|