@lowdefy/server 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.
Files changed (33) hide show
  1. package/client/Page.jsx +14 -1
  2. package/client/clientBuildImports.test.mjs +61 -0
  3. package/client/shouldReloadForBuild.js +48 -0
  4. package/client/shouldReloadForBuild.test.mjs +75 -0
  5. package/lib/client/auth/AuthConfigured.jsx +6 -7
  6. package/lib/server/auth/getAuthConfig.js +2 -0
  7. package/lib/server/log/createHandleError.js +9 -4
  8. package/lib/server/log/createHandleError.test.mjs +85 -0
  9. package/lib/server/log/createLogger.js +10 -0
  10. package/lib/server/log/createLogger.test.mjs +102 -0
  11. package/lib/server/log/logRequest.js +7 -1
  12. package/lib/server/log/maskCredentialKeys.js +37 -0
  13. package/lib/server/log/projectErrorForLog.js +55 -0
  14. package/lib/server/log/redactUrlQuery.js +75 -0
  15. package/lib/server/log/redactUrlQuery.test.mjs +73 -0
  16. package/lib/server/log/serializeErrorForLog.js +25 -0
  17. package/lib/server/log/serializeErrorForLog.test.mjs +325 -0
  18. package/lib/server/scrubSecrets.js +21 -0
  19. package/lib/server/sentry/captureSentryError.js +12 -0
  20. package/lib/server/sentry/captureSentryError.test.mjs +104 -0
  21. package/lib/server/sentry/initSentry.js +11 -0
  22. package/lib/server/sentry/initSentry.test.mjs +237 -0
  23. package/lib/server/sentry/scrubEvent.js +29 -0
  24. package/lib/server/sentry/scrubEvent.test.mjs +88 -0
  25. package/package.json +30 -31
  26. package/package.original.json +30 -31
  27. package/src/index.js +10 -0
  28. package/src/middleware/apiContext.js +3 -0
  29. package/src/middleware/apiContext.test.mjs +118 -0
  30. package/src/routes/apiPage.js +4 -1
  31. package/src/routes/apiPage.test.mjs +59 -0
  32. package/src/routes/cronForward.js +1 -1
  33. package/lib/server/apiWrapper.js +0 -87
@@ -0,0 +1,237 @@
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 http from 'node:http';
18
+
19
+ import { jest } from '@jest/globals';
20
+ import * as RealSentry from '@sentry/node';
21
+
22
+ const secret = 'planted/secret+value=1';
23
+
24
+ const envelopes = [];
25
+ function createStubTransport() {
26
+ return {
27
+ send: async (envelope) => {
28
+ envelopes.push(envelope);
29
+ return {};
30
+ },
31
+ flush: async () => true,
32
+ };
33
+ }
34
+
35
+ const httpIntegrations = [];
36
+
37
+ // The real SDK with its network transport swapped for memory, so each test reads the exact
38
+ // event Sentry would send, after the hooks initSentry installs.
39
+ jest.unstable_mockModule('@sentry/node', () => ({
40
+ ...RealSentry,
41
+ init: (options) => RealSentry.init({ ...options, transport: createStubTransport }),
42
+ httpIntegration: (options) => {
43
+ const integration = RealSentry.httpIntegration(options);
44
+ httpIntegrations.push(integration);
45
+ return integration;
46
+ },
47
+ }));
48
+ jest.unstable_mockModule('../../build/logger.js', () => ({ default: { sentry: {} } }));
49
+
50
+ process.env.SENTRY_DSN = 'https://public@o0.ingest.sentry.io/1';
51
+ process.env.LOWDEFY_SECRET_API_KEY = secret;
52
+
53
+ const { default: initSentryServer } = await import('./initSentry.js');
54
+
55
+ const enabled = initSentryServer();
56
+ const client = RealSentry.getClient();
57
+ const options = client.getOptions();
58
+
59
+ function sentEvents() {
60
+ return envelopes.flatMap(([, items]) =>
61
+ items.filter(([header]) => header.type === 'event').map(([, payload]) => payload)
62
+ );
63
+ }
64
+
65
+ beforeEach(() => {
66
+ envelopes.length = 0;
67
+ });
68
+
69
+ afterAll(async () => {
70
+ await RealSentry.close();
71
+ });
72
+
73
+ test('initSentry initializes Sentry when SENTRY_DSN is set', () => {
74
+ expect(enabled).toBe(true);
75
+ expect(client).toBeDefined();
76
+ });
77
+
78
+ test('initSentry installs exactly one Http integration, the one that attaches no request bodies', () => {
79
+ expect(httpIntegrations).toHaveLength(1);
80
+ const installedHttp = options.integrations.filter(({ name }) => name === 'Http');
81
+ expect(installedHttp).toHaveLength(1);
82
+ expect(installedHttp[0]).toBe(httpIntegrations[0]);
83
+ expect(client.getIntegrationByName('Http')).toBe(httpIntegrations[0]);
84
+ });
85
+
86
+ test('beforeSend replaces a planted secret in an exception value, a cause-chain exception value and extra', () => {
87
+ const event = {
88
+ event_id: 'a'.repeat(32),
89
+ level: 'error',
90
+ exception: {
91
+ values: [
92
+ {
93
+ type: 'AxiosError',
94
+ value: `connect failed for key ${secret}`,
95
+ mechanism: { type: 'chained', source: 'cause', exception_id: 1, parent_id: 0 },
96
+ },
97
+ {
98
+ type: 'RequestError',
99
+ value: `Request failed: ${encodeURIComponent(secret)}`,
100
+ stacktrace: { frames: [{ filename: 'app.js', function: 'handler', lineno: 12 }] },
101
+ mechanism: { type: 'generic', handled: true, exception_id: 0 },
102
+ },
103
+ ],
104
+ },
105
+ extra: {
106
+ configKey: 'key-1',
107
+ error: { name: 'RequestError', message: `failed with ${secret}`, received: [secret] },
108
+ },
109
+ tags: { requestId: 'req-1' },
110
+ };
111
+ const scrubbed = options.beforeSend(event, {});
112
+ expect(JSON.stringify(scrubbed)).not.toContain(secret);
113
+ expect(JSON.stringify(scrubbed)).not.toContain(encodeURIComponent(secret));
114
+ expect(scrubbed.exception.values[0].value).toEqual('connect failed for key [REDACTED]');
115
+ expect(scrubbed.exception.values[1].value).toEqual('Request failed: [REDACTED]');
116
+ expect(scrubbed.exception.values[1].stacktrace).toEqual(event.exception.values[1].stacktrace);
117
+ expect(scrubbed.extra).toEqual({
118
+ configKey: 'key-1',
119
+ error: { name: 'RequestError', message: 'failed with [REDACTED]', received: ['[REDACTED]'] },
120
+ });
121
+ expect(scrubbed.tags).toEqual({ requestId: 'req-1' });
122
+ });
123
+
124
+ test('beforeSend scrubs an exception captured through the SDK, cause chain included', async () => {
125
+ const cause = new Error(`upstream rejected ${secret}`);
126
+ RealSentry.captureException(new Error('Request failed.', { cause }), {
127
+ extra: { detail: `sent ${secret}` },
128
+ });
129
+ await RealSentry.flush();
130
+ const events = sentEvents();
131
+ expect(events).toHaveLength(1);
132
+ expect(JSON.stringify(events[0])).not.toContain(secret);
133
+ const values = events[0].exception.values.map(({ value }) => value);
134
+ expect(values).toEqual(['upstream rejected [REDACTED]', 'Request failed.']);
135
+ expect(events[0].extra.detail).toEqual('sent [REDACTED]');
136
+ });
137
+
138
+ test('beforeBreadcrumb replaces a planted secret in a breadcrumb URL', () => {
139
+ const breadcrumb = {
140
+ type: 'http',
141
+ category: 'http',
142
+ level: 'info',
143
+ data: {
144
+ url: `https://api.example.com/v1/items?api_key=${secret}`,
145
+ 'http.method': 'GET',
146
+ status_code: 200,
147
+ },
148
+ timestamp: 1700000000,
149
+ };
150
+ expect(options.beforeBreadcrumb(breadcrumb)).toEqual({
151
+ ...breadcrumb,
152
+ data: {
153
+ url: 'https://api.example.com/v1/items?api_key=[REDACTED]',
154
+ 'http.method': 'GET',
155
+ status_code: 200,
156
+ },
157
+ });
158
+ });
159
+
160
+ function createFetchSpan() {
161
+ return {
162
+ span_id: 'b'.repeat(16),
163
+ trace_id: 'c'.repeat(32),
164
+ parent_span_id: 'd'.repeat(16),
165
+ op: 'http.client',
166
+ description: `GET https://api.example.com/v1/items?api_key=${secret}`,
167
+ origin: 'auto.http.otel.node_fetch',
168
+ start_timestamp: 1700000000,
169
+ timestamp: 1700000001,
170
+ status: 'ok',
171
+ data: {
172
+ 'sentry.op': 'http.client',
173
+ 'http.method': 'GET',
174
+ url: `https://api.example.com/v1/items?api_key=${secret}`,
175
+ 'http.url': `https://api.example.com/v1/items?api_key=${secret}`,
176
+ 'http.query': `?api_key=${secret}`,
177
+ 'http.response.status_code': 200,
178
+ },
179
+ };
180
+ }
181
+
182
+ test('beforeSendSpan replaces a planted secret in a fetch span http.url and http.query', () => {
183
+ const scrubbed = options.beforeSendSpan(createFetchSpan());
184
+ expect(JSON.stringify(scrubbed)).not.toContain(secret);
185
+ expect(scrubbed.data['http.url']).toEqual('https://api.example.com/v1/items?api_key=[REDACTED]');
186
+ expect(scrubbed.data['http.query']).toEqual('?api_key=[REDACTED]');
187
+ expect(scrubbed.data['http.response.status_code']).toEqual(200);
188
+ });
189
+
190
+ test('beforeSendTransaction replaces a planted secret in a fetch span http.url and http.query', () => {
191
+ const transaction = {
192
+ type: 'transaction',
193
+ transaction: 'POST /api/endpoints/fetch-items',
194
+ start_timestamp: 1700000000,
195
+ timestamp: 1700000002,
196
+ contexts: {
197
+ trace: { span_id: 'd'.repeat(16), trace_id: 'c'.repeat(32), op: 'http.server' },
198
+ },
199
+ spans: [createFetchSpan()],
200
+ };
201
+ const scrubbed = options.beforeSendTransaction(transaction, {});
202
+ expect(JSON.stringify(scrubbed)).not.toContain(secret);
203
+ expect(scrubbed.spans[0].data['http.url']).toEqual(
204
+ 'https://api.example.com/v1/items?api_key=[REDACTED]'
205
+ );
206
+ expect(scrubbed.spans[0].data['http.query']).toEqual('?api_key=[REDACTED]');
207
+ expect(scrubbed.contexts).toEqual(transaction.contexts);
208
+ });
209
+
210
+ test('an error event captured during a request with a JSON body carries no request data', async () => {
211
+ const server = http.createServer((req, res) => {
212
+ const chunks = [];
213
+ req.on('data', (chunk) => chunks.push(chunk));
214
+ req.on('end', () => {
215
+ RealSentry.captureException(new Error('Endpoint failed.'));
216
+ res.end('{}');
217
+ });
218
+ });
219
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
220
+ const { port } = server.address();
221
+ try {
222
+ const response = await fetch(`http://127.0.0.1:${port}/api/endpoints/sign-in`, {
223
+ method: 'POST',
224
+ headers: { 'content-type': 'application/json' },
225
+ body: JSON.stringify({ email: 'user@example.com', password: 'planted-user-password' }),
226
+ });
227
+ await response.text();
228
+ } finally {
229
+ await new Promise((resolve) => server.close(resolve));
230
+ }
231
+ await RealSentry.flush();
232
+ const events = sentEvents();
233
+ expect(events).toHaveLength(1);
234
+ expect(events[0].request.url).toMatch(/\/api\/endpoints\/sign-in$/);
235
+ expect(events[0].request.data).toBeUndefined();
236
+ expect(JSON.stringify(events[0])).not.toContain('planted-user-password');
237
+ });
@@ -0,0 +1,29 @@
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 { mapPlainValues, type } from '@lowdefy/helpers';
18
+
19
+ import scrubSecrets from '../scrubSecrets.js';
20
+
21
+ // Class instances are left as they are: Sentry normalizes those before beforeSend
22
+ // runs, so the event hook still reaches their strings. Breadcrumbs reach
23
+ // beforeBreadcrumb un-normalized - a console breadcrumb holds the logged arguments
24
+ // as they are - so the walk must be cycle-safe, which mapPlainValues is.
25
+ function scrubEvent(value) {
26
+ return mapPlainValues(value, (item) => (type.isString(item) ? scrubSecrets(item) : item));
27
+ }
28
+
29
+ export default scrubEvent;
@@ -0,0 +1,88 @@
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
+ const secret = 'planted/secret+value=1';
18
+ const shortSecret = 'short77';
19
+
20
+ process.env.LOWDEFY_SECRET_API_KEY = secret;
21
+ process.env.LOWDEFY_SECRET_SHORT = shortSecret;
22
+
23
+ const { default: scrubEvent } = await import('./scrubEvent.js');
24
+
25
+ test('scrubEvent replaces a planted secret in strings at every depth of objects and arrays', () => {
26
+ expect(
27
+ scrubEvent({
28
+ message: `failed ${secret}`,
29
+ nested: { list: [`a ${secret}`, { deeper: [[secret]] }] },
30
+ })
31
+ ).toEqual({
32
+ message: 'failed [REDACTED]',
33
+ nested: { list: ['a [REDACTED]', { deeper: [['[REDACTED]']] }] },
34
+ });
35
+ });
36
+
37
+ test('scrubEvent leaves keys as they are, even a key holding a secret', () => {
38
+ expect(scrubEvent({ [secret]: 'value' })).toEqual({ [secret]: 'value' });
39
+ });
40
+
41
+ test('scrubEvent returns a copy and does not mutate the value it scrubs', () => {
42
+ const event = { extra: { detail: secret }, values: [secret] };
43
+ const scrubbed = scrubEvent(event);
44
+ expect(scrubbed).not.toBe(event);
45
+ expect(scrubbed.extra).not.toBe(event.extra);
46
+ expect(event).toEqual({ extra: { detail: secret }, values: [secret] });
47
+ });
48
+
49
+ test('scrubEvent returns numbers, booleans, null and undefined unchanged', () => {
50
+ expect(scrubEvent({ a: 1, b: true, c: null, d: undefined })).toEqual({
51
+ a: 1,
52
+ b: true,
53
+ c: null,
54
+ d: undefined,
55
+ });
56
+ expect(scrubEvent(null)).toBeNull();
57
+ });
58
+
59
+ test('scrubEvent scrubs a top-level string', () => {
60
+ expect(scrubEvent(`value ${secret}`)).toEqual('value [REDACTED]');
61
+ });
62
+
63
+ test('scrubEvent leaves a secret shorter than 8 characters alone', () => {
64
+ expect(scrubEvent({ detail: `value ${shortSecret}` })).toEqual({
65
+ detail: `value ${shortSecret}`,
66
+ });
67
+ });
68
+
69
+ test('scrubEvent returns class instances unchanged', () => {
70
+ class Client {
71
+ constructor() {
72
+ this.detail = secret;
73
+ }
74
+ }
75
+ const client = new Client();
76
+ const date = new Date(0);
77
+ const scrubbed = scrubEvent({ client, date });
78
+ expect(scrubbed.client).toBe(client);
79
+ expect(scrubbed.date).toBe(date);
80
+ });
81
+
82
+ test('scrubEvent copies a cycle as a cycle', () => {
83
+ const data = { message: secret };
84
+ data.self = data;
85
+ const scrubbed = scrubEvent({ data });
86
+ expect(scrubbed.data.message).toEqual('[REDACTED]');
87
+ expect(scrubbed.data.self).toBe(scrubbed.data);
88
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/server",
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,40 +40,39 @@
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",
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",
46
+ "@hono/node-server": "2.0.10",
47
+ "@lowdefy/actions-core": "6.1.0",
48
+ "@lowdefy/api": "6.1.0",
49
+ "@lowdefy/block-utils": "6.1.0",
50
+ "@lowdefy/blocks-aggrid": "6.1.0",
51
+ "@lowdefy/blocks-antd": "6.1.0",
52
+ "@lowdefy/blocks-antd-x": "6.1.0",
53
+ "@lowdefy/blocks-basic": "6.1.0",
54
+ "@lowdefy/blocks-echarts": "6.1.0",
55
+ "@lowdefy/blocks-files": "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/client": "6.1.0",
60
+ "@lowdefy/errors": "6.1.0",
61
+ "@lowdefy/helpers": "6.1.0",
62
+ "@lowdefy/layout": "6.1.0",
63
+ "@lowdefy/logger": "6.1.0",
64
+ "@lowdefy/node-utils": "6.1.0",
65
+ "@lowdefy/operators-js": "6.1.0",
66
+ "@lowdefy/plugin-next-auth": "6.1.0",
67
+ "@lowdefy/websockets-core": "6.1.0",
68
68
  "@sentry/browser": "10.57.0",
69
- "@sentry/nextjs": "8.53.0",
70
69
  "@sentry/node": "10.57.0",
71
70
  "@sentry/vite-plugin": "5.3.0",
72
71
  "@tailwindcss/postcss": "4.3.1",
73
72
  "@vitejs/plugin-react": "6.0.2",
74
73
  "antd": "6.3.1",
75
74
  "dayjs": "1.11.20",
76
- "hono": "4.12.25",
75
+ "hono": "4.13.5",
77
76
  "pino": "10.3.1",
78
77
  "react": "18.2.0",
79
78
  "react-dom": "18.2.0",
@@ -81,14 +80,13 @@
81
80
  "tailwindcss": "4.3.1",
82
81
  "uuid": "14.0.0",
83
82
  "vite": "8.0.16",
84
- "ws": "8.18.0"
83
+ "ws": "8.21.0"
85
84
  },
86
85
  "devDependencies": {
87
- "@lowdefy/build": "6.0.0",
88
- "@next/eslint-plugin-next": "16.1.6",
86
+ "@lowdefy/build": "6.1.0",
89
87
  "@tailwindcss/postcss": "4.3.1",
88
+ "jest": "28.1.3",
90
89
  "tailwindcss": "4.3.1",
91
- "webpack": "5.94.0",
92
90
  "yaml": "2.9.0",
93
91
  "yargs": "18.0.0"
94
92
  },
@@ -103,6 +101,7 @@
103
101
  "build": "cp package.json package.original.json || copy package.json package.original.json",
104
102
  "build:client": "vite build",
105
103
  "build:lowdefy": "node lowdefy/build.mjs",
106
- "start": "node src/index.js"
104
+ "start": "node src/index.js",
105
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
107
106
  }
108
107
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/server",
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",
@@ -42,45 +42,45 @@
42
42
  "build:client": "vite build",
43
43
  "build:lowdefy": "node lowdefy/build.mjs",
44
44
  "prepublishOnly": "pnpm build",
45
- "start": "node src/index.js"
45
+ "start": "node src/index.js",
46
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
46
47
  },
47
48
  "dependencies": {
48
49
  "@ant-design/cssinjs": "2.1.2",
49
50
  "@ant-design/x": "2.7.0",
50
- "@auth/core": "0.41.2",
51
+ "@auth/core": "0.41.3",
51
52
  "@hono/auth-js": "1.1.1",
52
53
  "@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",
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",
75
76
  "@sentry/browser": "10.57.0",
76
- "@sentry/nextjs": "8.53.0",
77
77
  "@sentry/node": "10.57.0",
78
78
  "@sentry/vite-plugin": "5.3.0",
79
79
  "@tailwindcss/postcss": "4.3.1",
80
80
  "@vitejs/plugin-react": "6.0.2",
81
81
  "antd": "6.3.1",
82
82
  "dayjs": "1.11.20",
83
- "hono": "4.12.25",
83
+ "hono": "4.13.5",
84
84
  "pino": "10.3.1",
85
85
  "react": "18.2.0",
86
86
  "react-dom": "18.2.0",
@@ -88,14 +88,13 @@
88
88
  "tailwindcss": "4.3.1",
89
89
  "uuid": "14.0.0",
90
90
  "vite": "8.0.16",
91
- "ws": "8.18.0"
91
+ "ws": "8.21.0"
92
92
  },
93
93
  "devDependencies": {
94
- "@lowdefy/build": "6.0.0",
95
- "@next/eslint-plugin-next": "16.1.6",
94
+ "@lowdefy/build": "6.1.0",
96
95
  "@tailwindcss/postcss": "4.3.1",
96
+ "jest": "28.1.3",
97
97
  "tailwindcss": "4.3.1",
98
- "webpack": "5.94.0",
99
98
  "yaml": "2.9.0",
100
99
  "yargs": "18.0.0"
101
100
  },
package/src/index.js CHANGED
@@ -18,6 +18,8 @@ import { serve } from '@hono/node-server';
18
18
  import * as Sentry from '@sentry/node';
19
19
  import { WebSocketServer } from 'ws';
20
20
 
21
+ import checkEnvironmentGuards from '@lowdefy/node-utils/checkEnvironmentGuards.js';
22
+
21
23
  import initSentryServer from '../lib/server/sentry/initSentry.js';
22
24
 
23
25
  // Auth.js v5 reads AUTH_URL but not NEXTAUTH_URL — alias the v4 variable
@@ -32,6 +34,14 @@ const sentryEnabled = initSentryServer();
32
34
  const { default: createApp } = await import('./app.js');
33
35
  const { default: createLogger } = await import('../lib/server/log/createLogger.js');
34
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
+ });
35
45
 
36
46
  const app = createApp();
37
47
  const logger = createLogger({ server: 'lowdefy' });
@@ -32,6 +32,7 @@ import i18nConfig from '../../lib/build/i18n.js';
32
32
  import jsMap from '../../build/plugins/operators/serverJsMap.js';
33
33
  import logRequest from '../../lib/server/log/logRequest.js';
34
34
  import loggerConfig from '../../lib/build/logger.js';
35
+ import scrubSecrets from '../../lib/server/scrubSecrets.js';
35
36
  import notifications, {
36
37
  interpolateProperties,
37
38
  renderEmail,
@@ -83,6 +84,7 @@ function apiContext() {
83
84
  interpolateProperties,
84
85
  jsMap,
85
86
  logger: createLogger({ rid }),
87
+ mode: 'prod',
86
88
  notifications,
87
89
  operators,
88
90
  renderEmail,
@@ -91,6 +93,7 @@ function apiContext() {
91
93
  method: c.req.method,
92
94
  hostname: c.req.header('host'),
93
95
  },
96
+ scrubSecrets,
94
97
  secrets,
95
98
  // On Vercel (fluid compute) the platform request context keeps the
96
99
  // invocation alive until waitUntil promises settle; on long-lived hosts