@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.
@@ -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
- import { serializer } from '@lowdefy/helpers';
30
-
31
- import rawLowdefyConfig from '../../../build/config.json';
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 (lowdefyConfig.basePath) {
38
- authConfigManager.setConfig({ basePath: `${lowdefyConfig.basePath}/api/auth` });
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.0.0",
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.2",
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.4",
46
+ "@hono/node-server": "2.0.10",
47
47
  "@hono/vite-dev-server": "0.26.0",
48
- "@lowdefy/actions-core": "6.0.0",
49
- "@lowdefy/api": "6.0.0",
50
- "@lowdefy/block-utils": "6.0.0",
51
- "@lowdefy/blocks-aggrid": "6.0.0",
52
- "@lowdefy/blocks-antd": "6.0.0",
53
- "@lowdefy/blocks-antd-x": "6.0.0",
54
- "@lowdefy/blocks-basic": "6.0.0",
55
- "@lowdefy/blocks-echarts": "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/build": "6.0.0",
60
- "@lowdefy/client": "6.0.0",
61
- "@lowdefy/connection-axios-http": "6.0.0",
62
- "@lowdefy/docs-content": "6.0.0",
63
- "@lowdefy/engine": "6.0.0",
64
- "@lowdefy/errors": "6.0.0",
65
- "@lowdefy/helpers": "6.0.0",
66
- "@lowdefy/layout": "6.0.0",
67
- "@lowdefy/logger": "6.0.0",
68
- "@lowdefy/node-utils": "6.0.0",
69
- "@lowdefy/operators-change-case": "6.0.0",
70
- "@lowdefy/operators-dayjs": "6.0.0",
71
- "@lowdefy/operators-diff": "6.0.0",
72
- "@lowdefy/operators-js": "6.0.0",
73
- "@lowdefy/operators-mql": "6.0.0",
74
- "@lowdefy/operators-nunjucks": "6.0.0",
75
- "@lowdefy/operators-uuid": "6.0.0",
76
- "@lowdefy/operators-yaml": "6.0.0",
77
- "@lowdefy/plugin-next-auth": "6.0.0",
78
- "@lowdefy/websockets-core": "6.0.0",
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.12.25",
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.6",
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.18.0",
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"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/server-dev",
3
- "version": "6.0.0",
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.2",
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.4",
52
+ "@hono/node-server": "2.0.10",
53
53
  "@hono/vite-dev-server": "0.26.0",
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-loaders": "6.0.0",
63
- "@lowdefy/blocks-markdown": "6.0.0",
64
- "@lowdefy/blocks-tiptap": "6.0.0",
65
- "@lowdefy/build": "6.0.0",
66
- "@lowdefy/client": "6.0.0",
67
- "@lowdefy/connection-axios-http": "6.0.0",
68
- "@lowdefy/docs-content": "6.0.0",
69
- "@lowdefy/engine": "6.0.0",
70
- "@lowdefy/errors": "6.0.0",
71
- "@lowdefy/helpers": "6.0.0",
72
- "@lowdefy/layout": "6.0.0",
73
- "@lowdefy/logger": "6.0.0",
74
- "@lowdefy/node-utils": "6.0.0",
75
- "@lowdefy/operators-change-case": "6.0.0",
76
- "@lowdefy/operators-dayjs": "6.0.0",
77
- "@lowdefy/operators-diff": "6.0.0",
78
- "@lowdefy/operators-js": "6.0.0",
79
- "@lowdefy/operators-mql": "6.0.0",
80
- "@lowdefy/operators-nunjucks": "6.0.0",
81
- "@lowdefy/operators-uuid": "6.0.0",
82
- "@lowdefy/operators-yaml": "6.0.0",
83
- "@lowdefy/plugin-next-auth": "6.0.0",
84
- "@lowdefy/websockets-core": "6.0.0",
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.12.25",
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.6",
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.18.0",
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('Top message.');
118
- expect(body['~e'].cause.message).toEqual('Cause message.');
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
- // The regression test for the real bug: the four inline `delete` statements this
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.stack).toBeUndefined();
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.awaited = true;
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'].awaited).toBe(true);
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('Top message.');
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 url in config.cron.environments. Vercel fires crons
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).