@lowdefy/server-dev 6.0.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/client/clientBuildImports.test.mjs +61 -0
- package/lib/client/auth/AuthConfigured.jsx +6 -7
- package/lib/server/auth/getAuthConfig.js +2 -0
- package/lib/server/createLowdefyContext.js +6 -0
- package/lib/server/createLowdefyContext.test.mjs +117 -0
- package/lib/server/log/createLogger.test.mjs +62 -0
- package/lib/server/scrubSecrets.js +23 -0
- package/package.json +38 -39
- package/package.original.json +38 -39
- package/src/middleware/errorHandler.test.mjs +23 -11
- package/src/routes/cronForward.js +1 -1
|
@@ -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
|
+
});
|
|
@@ -26,16 +26,15 @@ import {
|
|
|
26
26
|
useSession,
|
|
27
27
|
} from '@hono/auth-js/react';
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const lowdefyConfig = serializer.deserialize(rawLowdefyConfig);
|
|
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(/\/$/, '');
|
|
34
33
|
|
|
35
34
|
// @hono/auth-js/react configures its fetch paths through a module-level
|
|
36
35
|
// manager instead of SessionProvider props.
|
|
37
|
-
if (
|
|
38
|
-
authConfigManager.setConfig({ basePath: `${
|
|
36
|
+
if (basePath) {
|
|
37
|
+
authConfigManager.setConfig({ basePath: `${basePath}/api/auth` });
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
function Session({ children }) {
|
|
@@ -20,6 +20,7 @@ import { getSecretsFromEnv } from '@lowdefy/node-utils';
|
|
|
20
20
|
import adapters from '../../../build/plugins/auth/adapters.js';
|
|
21
21
|
import appMeta from '../../build/appMeta.js';
|
|
22
22
|
import authJson from '../../build/auth.js';
|
|
23
|
+
import config from '../../build/config.js';
|
|
23
24
|
import callbacks from '../../../build/plugins/auth/callbacks.js';
|
|
24
25
|
import events from '../../../build/plugins/auth/events.js';
|
|
25
26
|
import providers from '../../../build/plugins/auth/providers.js';
|
|
@@ -28,6 +29,7 @@ function getAuthConfig({ logger }) {
|
|
|
28
29
|
return getApiAuthConfig({
|
|
29
30
|
appMeta,
|
|
30
31
|
authJson,
|
|
32
|
+
config,
|
|
31
33
|
dev: true,
|
|
32
34
|
logger,
|
|
33
35
|
plugins: { adapters, callbacks, events, providers },
|
|
@@ -31,6 +31,7 @@ import getSession from './auth/session.js';
|
|
|
31
31
|
import getStrategyCaller from './auth/strategies.js';
|
|
32
32
|
import i18nConfig from '../build/i18n.js';
|
|
33
33
|
import logRequest from './log/logRequest.js';
|
|
34
|
+
import scrubSecrets from './scrubSecrets.js';
|
|
34
35
|
import notifications, {
|
|
35
36
|
interpolateProperties,
|
|
36
37
|
renderEmail,
|
|
@@ -84,6 +85,9 @@ async function createLowdefyContext({ c }) {
|
|
|
84
85
|
configDirectory: process.env.LOWDEFY_DIRECTORY_CONFIG || process.cwd(),
|
|
85
86
|
config,
|
|
86
87
|
connections,
|
|
88
|
+
// The dev server: environment-specific fallbacks that would be unsafe in
|
|
89
|
+
// production (a request-derived notification link origin) are allowed.
|
|
90
|
+
dev: true,
|
|
87
91
|
fileCache,
|
|
88
92
|
headers: c.req.header(),
|
|
89
93
|
// The deployment's own origin — detached endpoint calls loop back
|
|
@@ -93,6 +97,7 @@ async function createLowdefyContext({ c }) {
|
|
|
93
97
|
interpolateProperties,
|
|
94
98
|
jsMap,
|
|
95
99
|
logger: createLogger(),
|
|
100
|
+
mode: 'dev',
|
|
96
101
|
notifications,
|
|
97
102
|
operators,
|
|
98
103
|
renderEmail,
|
|
@@ -101,6 +106,7 @@ async function createLowdefyContext({ c }) {
|
|
|
101
106
|
method: c.req.method,
|
|
102
107
|
hostname: c.req.header('host'),
|
|
103
108
|
},
|
|
109
|
+
scrubSecrets,
|
|
104
110
|
secrets,
|
|
105
111
|
websockets,
|
|
106
112
|
};
|
|
@@ -0,0 +1,117 @@
|
|
|
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 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
|
|
21
|
+
import { jest } from '@jest/globals';
|
|
22
|
+
import { projectCaughtError } from '@lowdefy/helpers';
|
|
23
|
+
import { operatorsServer } from '@lowdefy/operators-js';
|
|
24
|
+
|
|
25
|
+
const secret = 'planted-dev-secret';
|
|
26
|
+
process.env.LOWDEFY_SECRET_TEST = secret;
|
|
27
|
+
|
|
28
|
+
jest.unstable_mockModule('@lowdefy/api', () => ({ createApiContext: jest.fn() }));
|
|
29
|
+
jest.unstable_mockModule('../build/appMeta.js', () => ({ default: {} }));
|
|
30
|
+
jest.unstable_mockModule('../build/config.js', () => ({ default: {} }));
|
|
31
|
+
jest.unstable_mockModule('../build/i18n.js', () => ({ default: {} }));
|
|
32
|
+
jest.unstable_mockModule('./log/createHandleError.js', () => ({
|
|
33
|
+
default: jest.fn(() => jest.fn()),
|
|
34
|
+
}));
|
|
35
|
+
jest.unstable_mockModule('./log/createLogger.js', () => ({
|
|
36
|
+
default: jest.fn(() => ({ debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() })),
|
|
37
|
+
}));
|
|
38
|
+
jest.unstable_mockModule('./fileCache.js', () => ({ default: {} }));
|
|
39
|
+
jest.unstable_mockModule('./auth/session.js', () => ({ default: jest.fn(async () => undefined) }));
|
|
40
|
+
jest.unstable_mockModule('./auth/strategies.js', () => ({ default: jest.fn(async () => null) }));
|
|
41
|
+
jest.unstable_mockModule('./log/logRequest.js', () => ({ default: jest.fn() }));
|
|
42
|
+
|
|
43
|
+
// createLowdefyContext statically imports the app build's plugin artifacts,
|
|
44
|
+
// which only exist in a built app (build/** is gitignored). Materialize the
|
|
45
|
+
// minimal set it needs — same fixture strategy as auth/getDevSession.test.mjs
|
|
46
|
+
// — so the import resolves; clean up only what this test created.
|
|
47
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
const pluginsDir = path.resolve(dirname, '../../build/plugins');
|
|
49
|
+
const fixtures = [
|
|
50
|
+
{ file: 'agents.js', content: 'export default {};\n' },
|
|
51
|
+
{ file: 'connections.js', content: 'export default {};\n' },
|
|
52
|
+
{
|
|
53
|
+
file: 'notifications.js',
|
|
54
|
+
content: 'export default {};\nexport const interpolateProperties = () => {};\nexport const renderEmail = () => {};\n',
|
|
55
|
+
},
|
|
56
|
+
{ file: path.join('operators', 'server.js'), content: 'export default {};\n' },
|
|
57
|
+
{ file: path.join('operators', 'serverJsMap.js'), content: 'export default {};\n' },
|
|
58
|
+
{ file: 'websockets.js', content: 'export default {};\n' },
|
|
59
|
+
].map(({ file, content }) => ({ absolutePath: path.join(pluginsDir, file), content }));
|
|
60
|
+
|
|
61
|
+
const createdFixtures = [];
|
|
62
|
+
fixtures.forEach(({ absolutePath, content }) => {
|
|
63
|
+
if (!fs.existsSync(absolutePath)) {
|
|
64
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
65
|
+
fs.writeFileSync(absolutePath, content);
|
|
66
|
+
createdFixtures.push(absolutePath);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// A checkout that has built an app already holds real artifacts here, which import plugins this
|
|
71
|
+
// package does not depend on, so the stubs are also mocked over whatever is on disk.
|
|
72
|
+
fixtures.forEach(({ absolutePath }) => {
|
|
73
|
+
jest.unstable_mockModule(absolutePath, () => ({
|
|
74
|
+
default: {},
|
|
75
|
+
interpolateProperties: () => {},
|
|
76
|
+
renderEmail: () => {},
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterAll(() => {
|
|
81
|
+
createdFixtures.forEach((absolutePath) => fs.rmSync(absolutePath, { force: true }));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const { default: createLowdefyContext } = await import('./createLowdefyContext.js');
|
|
85
|
+
|
|
86
|
+
function createHonoContext({ path: reqPath = '/api/request/foo' } = {}) {
|
|
87
|
+
return {
|
|
88
|
+
req: {
|
|
89
|
+
header: (name) => (name ? undefined : {}),
|
|
90
|
+
path: reqPath,
|
|
91
|
+
method: 'POST',
|
|
92
|
+
url: `http://localhost${reqPath}`,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test('createLowdefyContext sets mode to dev', async () => {
|
|
98
|
+
const context = await createLowdefyContext({ c: createHonoContext() });
|
|
99
|
+
expect(context.mode).toEqual('dev');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('createLowdefyContext scrubSecrets redacts a planted secret', async () => {
|
|
103
|
+
const context = await createLowdefyContext({ c: createHonoContext() });
|
|
104
|
+
expect(context.scrubSecrets(`token ${secret} end`)).toEqual('token [REDACTED] end');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// controlTry builds the value `_error` reads from context.scrubSecrets, so a dev context has to
|
|
108
|
+
// scrub a caught message the same way the production server does.
|
|
109
|
+
test('_error on a dev context reads a caught message with a planted secret scrubbed', async () => {
|
|
110
|
+
const context = await createLowdefyContext({ c: createHonoContext() });
|
|
111
|
+
const error = projectCaughtError(new Error(`Connect failed with ${secret}.`), {
|
|
112
|
+
scrub: context.scrubSecrets,
|
|
113
|
+
});
|
|
114
|
+
expect(operatorsServer._error({ error, location: 'test', params: 'message' })).toEqual(
|
|
115
|
+
'Connect failed with [REDACTED].'
|
|
116
|
+
);
|
|
117
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { jest } from '@jest/globals';
|
|
18
|
+
import { createNodeLogger } from '@lowdefy/logger/node';
|
|
19
|
+
import { serializer } from '@lowdefy/helpers';
|
|
20
|
+
|
|
21
|
+
const secret = 'planted-dev-secret-value';
|
|
22
|
+
|
|
23
|
+
const lines = [];
|
|
24
|
+
const destination = {
|
|
25
|
+
write(line) {
|
|
26
|
+
lines.push(line);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// The CLI's dev terminal is parsed from this logger's output, so it must keep the full default
|
|
31
|
+
// serialization; route the real logger to memory to read the exact emitted line.
|
|
32
|
+
jest.unstable_mockModule('@lowdefy/logger/node', () => ({
|
|
33
|
+
createNodeLogger: (options) => createNodeLogger({ ...options, destination }),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
process.env.LOWDEFY_SECRET_API_KEY = secret;
|
|
37
|
+
|
|
38
|
+
const { default: createLogger } = await import('./createLogger.js');
|
|
39
|
+
|
|
40
|
+
function createAxiosError() {
|
|
41
|
+
const error = new Error(`Request failed for key ${secret}`);
|
|
42
|
+
error.name = 'AxiosError';
|
|
43
|
+
error.code = 'ERR_BAD_REQUEST';
|
|
44
|
+
error.config = {
|
|
45
|
+
auth: { username: 'svc-user', password: 'planted-config-password' },
|
|
46
|
+
params: { api_key: 'planted-config-param' },
|
|
47
|
+
headers: { Authorization: 'Bearer planted-config-header' },
|
|
48
|
+
};
|
|
49
|
+
return error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test('dev logger writes an axios error with full default serialization and no scrub', () => {
|
|
53
|
+
const error = createAxiosError();
|
|
54
|
+
createLogger().error({ err: error }, error.message);
|
|
55
|
+
expect(lines).toHaveLength(1);
|
|
56
|
+
const { err, msg } = JSON.parse(lines[0]);
|
|
57
|
+
expect(err).toEqual(JSON.parse(JSON.stringify(serializer.serialize(error)['~e'])));
|
|
58
|
+
expect(err.config.auth.password).toBe('planted-config-password');
|
|
59
|
+
expect(err.config.params.api_key).toBe('planted-config-param');
|
|
60
|
+
expect(err.config.headers.Authorization).toBe('Bearer planted-config-header');
|
|
61
|
+
expect(msg).toBe(`Request failed for key ${secret}`);
|
|
62
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
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 { createSecretScrubber, getSecretsFromEnv } from '@lowdefy/node-utils';
|
|
18
|
+
|
|
19
|
+
// Dev logs stay unscrubbed, but _error reads this off the context like every
|
|
20
|
+
// other server package, so a thrown message behaves identically in every mode.
|
|
21
|
+
const scrubSecrets = createSecretScrubber({ secrets: getSecretsFromEnv() });
|
|
22
|
+
|
|
23
|
+
export default scrubSecrets;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/server-dev",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -40,42 +40,42 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@ant-design/cssinjs": "2.1.2",
|
|
42
42
|
"@ant-design/x": "2.7.0",
|
|
43
|
-
"@auth/core": "0.41.
|
|
43
|
+
"@auth/core": "0.41.3",
|
|
44
44
|
"@hono/auth-js": "1.1.1",
|
|
45
45
|
"@hono/mcp": "0.3.0",
|
|
46
|
-
"@hono/node-server": "2.0.
|
|
46
|
+
"@hono/node-server": "2.0.10",
|
|
47
47
|
"@hono/vite-dev-server": "0.26.0",
|
|
48
|
-
"@lowdefy/actions-core": "6.
|
|
49
|
-
"@lowdefy/api": "6.
|
|
50
|
-
"@lowdefy/block-utils": "6.
|
|
51
|
-
"@lowdefy/blocks-aggrid": "6.
|
|
52
|
-
"@lowdefy/blocks-antd": "6.
|
|
53
|
-
"@lowdefy/blocks-antd-x": "6.
|
|
54
|
-
"@lowdefy/blocks-basic": "6.
|
|
55
|
-
"@lowdefy/blocks-echarts": "6.
|
|
56
|
-
"@lowdefy/blocks-loaders": "6.
|
|
57
|
-
"@lowdefy/blocks-markdown": "6.
|
|
58
|
-
"@lowdefy/blocks-tiptap": "6.
|
|
59
|
-
"@lowdefy/build": "6.
|
|
60
|
-
"@lowdefy/client": "6.
|
|
61
|
-
"@lowdefy/connection-axios-http": "6.
|
|
62
|
-
"@lowdefy/docs-content": "6.
|
|
63
|
-
"@lowdefy/engine": "6.
|
|
64
|
-
"@lowdefy/errors": "6.
|
|
65
|
-
"@lowdefy/helpers": "6.
|
|
66
|
-
"@lowdefy/layout": "6.
|
|
67
|
-
"@lowdefy/logger": "6.
|
|
68
|
-
"@lowdefy/node-utils": "6.
|
|
69
|
-
"@lowdefy/operators-change-case": "6.
|
|
70
|
-
"@lowdefy/operators-dayjs": "6.
|
|
71
|
-
"@lowdefy/operators-diff": "6.
|
|
72
|
-
"@lowdefy/operators-js": "6.
|
|
73
|
-
"@lowdefy/operators-mql": "6.
|
|
74
|
-
"@lowdefy/operators-nunjucks": "6.
|
|
75
|
-
"@lowdefy/operators-uuid": "6.
|
|
76
|
-
"@lowdefy/operators-yaml": "6.
|
|
77
|
-
"@lowdefy/plugin-next-auth": "6.
|
|
78
|
-
"@lowdefy/websockets-core": "6.
|
|
48
|
+
"@lowdefy/actions-core": "6.1.0",
|
|
49
|
+
"@lowdefy/api": "6.1.0",
|
|
50
|
+
"@lowdefy/block-utils": "6.1.0",
|
|
51
|
+
"@lowdefy/blocks-aggrid": "6.1.0",
|
|
52
|
+
"@lowdefy/blocks-antd": "6.1.0",
|
|
53
|
+
"@lowdefy/blocks-antd-x": "6.1.0",
|
|
54
|
+
"@lowdefy/blocks-basic": "6.1.0",
|
|
55
|
+
"@lowdefy/blocks-echarts": "6.1.0",
|
|
56
|
+
"@lowdefy/blocks-loaders": "6.1.0",
|
|
57
|
+
"@lowdefy/blocks-markdown": "6.1.0",
|
|
58
|
+
"@lowdefy/blocks-tiptap": "6.1.0",
|
|
59
|
+
"@lowdefy/build": "6.1.0",
|
|
60
|
+
"@lowdefy/client": "6.1.0",
|
|
61
|
+
"@lowdefy/connection-axios-http": "6.1.0",
|
|
62
|
+
"@lowdefy/docs-content": "6.1.0",
|
|
63
|
+
"@lowdefy/engine": "6.1.0",
|
|
64
|
+
"@lowdefy/errors": "6.1.0",
|
|
65
|
+
"@lowdefy/helpers": "6.1.0",
|
|
66
|
+
"@lowdefy/layout": "6.1.0",
|
|
67
|
+
"@lowdefy/logger": "6.1.0",
|
|
68
|
+
"@lowdefy/node-utils": "6.1.0",
|
|
69
|
+
"@lowdefy/operators-change-case": "6.1.0",
|
|
70
|
+
"@lowdefy/operators-dayjs": "6.1.0",
|
|
71
|
+
"@lowdefy/operators-diff": "6.1.0",
|
|
72
|
+
"@lowdefy/operators-js": "6.1.0",
|
|
73
|
+
"@lowdefy/operators-mql": "6.1.0",
|
|
74
|
+
"@lowdefy/operators-nunjucks": "6.1.0",
|
|
75
|
+
"@lowdefy/operators-uuid": "6.1.0",
|
|
76
|
+
"@lowdefy/operators-yaml": "6.1.0",
|
|
77
|
+
"@lowdefy/plugin-next-auth": "6.1.0",
|
|
78
|
+
"@lowdefy/websockets-core": "6.1.0",
|
|
79
79
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
80
80
|
"@tailwindcss/postcss": "4.3.1",
|
|
81
81
|
"@vitejs/plugin-react": "6.0.2",
|
|
@@ -83,12 +83,12 @@
|
|
|
83
83
|
"chokidar": "3.5.3",
|
|
84
84
|
"dayjs": "1.11.20",
|
|
85
85
|
"dotenv": "16.3.1",
|
|
86
|
-
"hono": "4.
|
|
86
|
+
"hono": "4.13.5",
|
|
87
87
|
"html-to-image": "1.11.13",
|
|
88
88
|
"opener": "1.5.2",
|
|
89
89
|
"pino": "10.3.1",
|
|
90
90
|
"playwright-core": "1.59.1",
|
|
91
|
-
"postcss": "8.5.
|
|
91
|
+
"postcss": "8.5.23",
|
|
92
92
|
"process": "0.11.10",
|
|
93
93
|
"react": "18.2.0",
|
|
94
94
|
"react-dom": "18.2.0",
|
|
@@ -98,14 +98,13 @@
|
|
|
98
98
|
"tinykeys": "3.0.0",
|
|
99
99
|
"uuid": "14.0.0",
|
|
100
100
|
"vite": "8.0.16",
|
|
101
|
-
"ws": "8.
|
|
101
|
+
"ws": "8.21.0",
|
|
102
102
|
"yaml": "2.9.0",
|
|
103
103
|
"yargs": "18.0.0",
|
|
104
104
|
"zod": "3.25.76"
|
|
105
105
|
},
|
|
106
106
|
"devDependencies": {
|
|
107
|
-
"jest": "28.1.3"
|
|
108
|
-
"webpack": "5.94.0"
|
|
107
|
+
"jest": "28.1.3"
|
|
109
108
|
},
|
|
110
109
|
"engines": {
|
|
111
110
|
"node": ">=22"
|
package/package.original.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/server-dev",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -46,42 +46,42 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@ant-design/cssinjs": "2.1.2",
|
|
48
48
|
"@ant-design/x": "2.7.0",
|
|
49
|
-
"@auth/core": "0.41.
|
|
49
|
+
"@auth/core": "0.41.3",
|
|
50
50
|
"@hono/auth-js": "1.1.1",
|
|
51
51
|
"@hono/mcp": "0.3.0",
|
|
52
|
-
"@hono/node-server": "2.0.
|
|
52
|
+
"@hono/node-server": "2.0.10",
|
|
53
53
|
"@hono/vite-dev-server": "0.26.0",
|
|
54
|
-
"@lowdefy/actions-core": "6.
|
|
55
|
-
"@lowdefy/api": "6.
|
|
56
|
-
"@lowdefy/block-utils": "6.
|
|
57
|
-
"@lowdefy/blocks-aggrid": "6.
|
|
58
|
-
"@lowdefy/blocks-antd": "6.
|
|
59
|
-
"@lowdefy/blocks-antd-x": "6.
|
|
60
|
-
"@lowdefy/blocks-basic": "6.
|
|
61
|
-
"@lowdefy/blocks-echarts": "6.
|
|
62
|
-
"@lowdefy/blocks-loaders": "6.
|
|
63
|
-
"@lowdefy/blocks-markdown": "6.
|
|
64
|
-
"@lowdefy/blocks-tiptap": "6.
|
|
65
|
-
"@lowdefy/build": "6.
|
|
66
|
-
"@lowdefy/client": "6.
|
|
67
|
-
"@lowdefy/connection-axios-http": "6.
|
|
68
|
-
"@lowdefy/docs-content": "6.
|
|
69
|
-
"@lowdefy/engine": "6.
|
|
70
|
-
"@lowdefy/errors": "6.
|
|
71
|
-
"@lowdefy/helpers": "6.
|
|
72
|
-
"@lowdefy/layout": "6.
|
|
73
|
-
"@lowdefy/logger": "6.
|
|
74
|
-
"@lowdefy/node-utils": "6.
|
|
75
|
-
"@lowdefy/operators-change-case": "6.
|
|
76
|
-
"@lowdefy/operators-dayjs": "6.
|
|
77
|
-
"@lowdefy/operators-diff": "6.
|
|
78
|
-
"@lowdefy/operators-js": "6.
|
|
79
|
-
"@lowdefy/operators-mql": "6.
|
|
80
|
-
"@lowdefy/operators-nunjucks": "6.
|
|
81
|
-
"@lowdefy/operators-uuid": "6.
|
|
82
|
-
"@lowdefy/operators-yaml": "6.
|
|
83
|
-
"@lowdefy/plugin-next-auth": "6.
|
|
84
|
-
"@lowdefy/websockets-core": "6.
|
|
54
|
+
"@lowdefy/actions-core": "6.1.0",
|
|
55
|
+
"@lowdefy/api": "6.1.0",
|
|
56
|
+
"@lowdefy/block-utils": "6.1.0",
|
|
57
|
+
"@lowdefy/blocks-aggrid": "6.1.0",
|
|
58
|
+
"@lowdefy/blocks-antd": "6.1.0",
|
|
59
|
+
"@lowdefy/blocks-antd-x": "6.1.0",
|
|
60
|
+
"@lowdefy/blocks-basic": "6.1.0",
|
|
61
|
+
"@lowdefy/blocks-echarts": "6.1.0",
|
|
62
|
+
"@lowdefy/blocks-loaders": "6.1.0",
|
|
63
|
+
"@lowdefy/blocks-markdown": "6.1.0",
|
|
64
|
+
"@lowdefy/blocks-tiptap": "6.1.0",
|
|
65
|
+
"@lowdefy/build": "6.1.0",
|
|
66
|
+
"@lowdefy/client": "6.1.0",
|
|
67
|
+
"@lowdefy/connection-axios-http": "6.1.0",
|
|
68
|
+
"@lowdefy/docs-content": "6.1.0",
|
|
69
|
+
"@lowdefy/engine": "6.1.0",
|
|
70
|
+
"@lowdefy/errors": "6.1.0",
|
|
71
|
+
"@lowdefy/helpers": "6.1.0",
|
|
72
|
+
"@lowdefy/layout": "6.1.0",
|
|
73
|
+
"@lowdefy/logger": "6.1.0",
|
|
74
|
+
"@lowdefy/node-utils": "6.1.0",
|
|
75
|
+
"@lowdefy/operators-change-case": "6.1.0",
|
|
76
|
+
"@lowdefy/operators-dayjs": "6.1.0",
|
|
77
|
+
"@lowdefy/operators-diff": "6.1.0",
|
|
78
|
+
"@lowdefy/operators-js": "6.1.0",
|
|
79
|
+
"@lowdefy/operators-mql": "6.1.0",
|
|
80
|
+
"@lowdefy/operators-nunjucks": "6.1.0",
|
|
81
|
+
"@lowdefy/operators-uuid": "6.1.0",
|
|
82
|
+
"@lowdefy/operators-yaml": "6.1.0",
|
|
83
|
+
"@lowdefy/plugin-next-auth": "6.1.0",
|
|
84
|
+
"@lowdefy/websockets-core": "6.1.0",
|
|
85
85
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
86
86
|
"@tailwindcss/postcss": "4.3.1",
|
|
87
87
|
"@vitejs/plugin-react": "6.0.2",
|
|
@@ -89,12 +89,12 @@
|
|
|
89
89
|
"chokidar": "3.5.3",
|
|
90
90
|
"dayjs": "1.11.20",
|
|
91
91
|
"dotenv": "16.3.1",
|
|
92
|
-
"hono": "4.
|
|
92
|
+
"hono": "4.13.5",
|
|
93
93
|
"html-to-image": "1.11.13",
|
|
94
94
|
"opener": "1.5.2",
|
|
95
95
|
"pino": "10.3.1",
|
|
96
96
|
"playwright-core": "1.59.1",
|
|
97
|
-
"postcss": "8.5.
|
|
97
|
+
"postcss": "8.5.23",
|
|
98
98
|
"process": "0.11.10",
|
|
99
99
|
"react": "18.2.0",
|
|
100
100
|
"react-dom": "18.2.0",
|
|
@@ -104,14 +104,13 @@
|
|
|
104
104
|
"tinykeys": "3.0.0",
|
|
105
105
|
"uuid": "14.0.0",
|
|
106
106
|
"vite": "8.0.16",
|
|
107
|
-
"ws": "8.
|
|
107
|
+
"ws": "8.21.0",
|
|
108
108
|
"yaml": "2.9.0",
|
|
109
109
|
"yargs": "18.0.0",
|
|
110
110
|
"zod": "3.25.76"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
|
-
"jest": "28.1.3"
|
|
114
|
-
"webpack": "5.94.0"
|
|
113
|
+
"jest": "28.1.3"
|
|
115
114
|
},
|
|
116
115
|
"engines": {
|
|
117
116
|
"node": ">=22"
|
|
@@ -114,8 +114,8 @@ test('errorHandler returns 500 with the serialized error envelope on an api path
|
|
|
114
114
|
expect(res.status).toEqual(500);
|
|
115
115
|
const body = await res.json();
|
|
116
116
|
expect(body['~e'].name).toEqual('Error');
|
|
117
|
-
expect(body['~e'].message).toEqual('
|
|
118
|
-
expect(body['~e'].cause
|
|
117
|
+
expect(body['~e'].message).toEqual('Something went wrong.');
|
|
118
|
+
expect(body['~e'].cause).toBeUndefined();
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
test('errorHandler omits stack and received from the 500 envelope at the top level', async () => {
|
|
@@ -129,19 +129,31 @@ test('errorHandler omits stack and received from the 500 envelope at the top lev
|
|
|
129
129
|
expect(body['~e'].received).toBeUndefined();
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
// replaces only ever reached depth 0, so a `received` or `stack` on the cause
|
|
134
|
-
// still crossed the wire. The policy is now applied at every error node.
|
|
135
|
-
test('errorHandler omits stack and received from the 500 envelope on the cause at depth 1', async () => {
|
|
132
|
+
test('errorHandler omits the cause chain from the 500 envelope', async () => {
|
|
136
133
|
const res = await createApp({
|
|
137
134
|
error: createErrorWithCause(),
|
|
138
135
|
logger: createLogger(),
|
|
139
136
|
}).request('/api/request/getUsers');
|
|
140
137
|
|
|
141
138
|
const body = await res.json();
|
|
142
|
-
expect(body['~e'].cause
|
|
143
|
-
expect(body['~e'].cause.received).toBeUndefined();
|
|
139
|
+
expect(body['~e'].cause).toBeUndefined();
|
|
144
140
|
expect(JSON.stringify(body)).not.toContain(SECRET);
|
|
141
|
+
expect(JSON.stringify(body)).not.toContain('Cause message.');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('errorHandler adds devError with the full error to the 500 envelope in dev mode', async () => {
|
|
145
|
+
const context = { handleError: jest.fn(), mode: 'dev', rid: 'rid-1' };
|
|
146
|
+
const res = await createApp({
|
|
147
|
+
context,
|
|
148
|
+
error: createErrorWithCause(),
|
|
149
|
+
logger: createLogger(),
|
|
150
|
+
}).request('/api/request/getUsers');
|
|
151
|
+
|
|
152
|
+
const body = await res.json();
|
|
153
|
+
expect(body['~e'].message).toEqual('Something went wrong.');
|
|
154
|
+
expect(body.devError['~e'].message).toEqual('Top message.');
|
|
155
|
+
expect(body.devError['~e'].requestId).toEqual('rid-1');
|
|
156
|
+
expect(body.devError['~e'].cause.message).toEqual('Cause message.');
|
|
145
157
|
});
|
|
146
158
|
|
|
147
159
|
test('errorHandler keeps configKey on the 500 envelope so the fault can be located in config', async () => {
|
|
@@ -168,7 +180,7 @@ test('errorHandler awaits context.handleError before serializing the response',
|
|
|
168
180
|
const context = {
|
|
169
181
|
handleError: jest.fn(async (error) => {
|
|
170
182
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
171
|
-
error.
|
|
183
|
+
error.handled = true;
|
|
172
184
|
}),
|
|
173
185
|
};
|
|
174
186
|
const error = createErrorWithCause();
|
|
@@ -179,7 +191,7 @@ test('errorHandler awaits context.handleError before serializing the response',
|
|
|
179
191
|
expect(context.handleError).toHaveBeenCalledTimes(1);
|
|
180
192
|
expect(context.handleError).toHaveBeenCalledWith(error);
|
|
181
193
|
const body = await res.json();
|
|
182
|
-
expect(body['~e'].
|
|
194
|
+
expect(body['~e'].handled).toBe(true);
|
|
183
195
|
});
|
|
184
196
|
|
|
185
197
|
// createHandleError sets error.handled once it has logged. The client reads it
|
|
@@ -220,7 +232,7 @@ test('errorHandler treats a basePath prefixed api path as an api path', async ()
|
|
|
220
232
|
|
|
221
233
|
expect(res.status).toEqual(500);
|
|
222
234
|
const body = await res.json();
|
|
223
|
-
expect(body['~e'].message).toEqual('
|
|
235
|
+
expect(body['~e'].message).toEqual('Something went wrong.');
|
|
224
236
|
});
|
|
225
237
|
|
|
226
238
|
test('errorHandler treats a basePath prefixed page path as a page path', async () => {
|
|
@@ -20,7 +20,7 @@ import getPathSegments from '../lib/getPathSegments.js';
|
|
|
20
20
|
|
|
21
21
|
// Triggered by Vercel Cron on the production deployment: an HTTP GET to
|
|
22
22
|
// /api/cron-forward/<environment>/<endpointId>, registered by `lowdefy vercel-output` for the
|
|
23
|
-
// schedules of every environment declared with a
|
|
23
|
+
// schedules of every environment declared with a cron.secret in config.environments. Vercel fires crons
|
|
24
24
|
// only on production, so production pings that environment's own /api/cron/<endpointId> (with the
|
|
25
25
|
// environment's CRON_SECRET) and answers immediately. Same transport auth as cron (CRON_SECRET,
|
|
26
26
|
// fails closed).
|