@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
|
@@ -14,30 +14,43 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import * as Sentry from '@sentry/
|
|
17
|
+
import * as Sentry from '@sentry/node';
|
|
18
18
|
|
|
19
19
|
import loggerConfig from '../../build/logger.js';
|
|
20
|
+
import scrubEvent from './scrubEvent.js';
|
|
20
21
|
|
|
22
|
+
// Returns true when Sentry was initialized, so the caller can log it through
|
|
23
|
+
// the structured logger — this module runs before the logger exists.
|
|
21
24
|
function initSentryServer() {
|
|
22
25
|
// No-op if SENTRY_DSN not set
|
|
23
26
|
if (!process.env.SENTRY_DSN) {
|
|
24
|
-
return;
|
|
27
|
+
return false;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const sentryConfig = loggerConfig.sentry || {};
|
|
28
31
|
|
|
29
32
|
// No-op if server logging is explicitly disabled
|
|
30
33
|
if (sentryConfig.server === false) {
|
|
31
|
-
return;
|
|
34
|
+
return false;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
Sentry.init({
|
|
35
38
|
dsn: process.env.SENTRY_DSN,
|
|
36
39
|
environment: sentryConfig.environment || process.env.NODE_ENV || 'production',
|
|
37
40
|
tracesSampleRate: sentryConfig.tracesSampleRate ?? 0.1,
|
|
41
|
+
// Every path out to Sentry gets the log's secret scrub: error events with their cause
|
|
42
|
+
// chain, and the spans and breadcrumbs that carry outgoing URLs with their query strings.
|
|
43
|
+
beforeSend: (event) => scrubEvent(event),
|
|
44
|
+
beforeSendTransaction: (event) => scrubEvent(event),
|
|
45
|
+
beforeSendSpan: (span) => scrubEvent(span),
|
|
46
|
+
beforeBreadcrumb: (breadcrumb) => scrubEvent(breadcrumb),
|
|
47
|
+
// Incoming request bodies are attached to error events by default - an endpoint payload,
|
|
48
|
+
// a sign-in password - and no value scrub can recognise them. This replaces the default
|
|
49
|
+
// Http integration by name rather than adding a second one.
|
|
50
|
+
integrations: [Sentry.httpIntegration({ maxIncomingRequestBodySize: 'none' })],
|
|
38
51
|
});
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
return true;
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
export default initSentryServer;
|
|
@@ -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": "
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -28,68 +28,80 @@
|
|
|
28
28
|
"files": [
|
|
29
29
|
"lib/*",
|
|
30
30
|
"lowdefy/*",
|
|
31
|
-
"pages/*",
|
|
32
31
|
"public_default/*",
|
|
33
|
-
"next.config.js",
|
|
34
|
-
"postcss.config.js",
|
|
35
32
|
"package.original.json",
|
|
36
33
|
".eslintrc.yaml",
|
|
37
|
-
".npmrc"
|
|
34
|
+
".npmrc",
|
|
35
|
+
"src/*",
|
|
36
|
+
"client/*",
|
|
37
|
+
"vite.config.js",
|
|
38
|
+
"postcss.config.cjs"
|
|
38
39
|
],
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"@ant-design/cssinjs": "2.1.2",
|
|
41
42
|
"@ant-design/x": "2.7.0",
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
44
|
-
"@
|
|
45
|
-
"@
|
|
46
|
-
"@lowdefy/
|
|
47
|
-
"@lowdefy/
|
|
48
|
-
"@lowdefy/
|
|
49
|
-
"@lowdefy/blocks-
|
|
50
|
-
"@lowdefy/
|
|
51
|
-
"@lowdefy/
|
|
52
|
-
"@lowdefy/
|
|
53
|
-
"@lowdefy/
|
|
54
|
-
"@lowdefy/
|
|
55
|
-
"@lowdefy/
|
|
56
|
-
"@lowdefy/
|
|
57
|
-
"@lowdefy/
|
|
58
|
-
"@
|
|
59
|
-
"@
|
|
43
|
+
"@auth/core": "0.41.3",
|
|
44
|
+
"@hono/auth-js": "1.1.1",
|
|
45
|
+
"@hono/mcp": "0.3.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
|
+
"@sentry/browser": "10.57.0",
|
|
69
|
+
"@sentry/node": "10.57.0",
|
|
70
|
+
"@sentry/vite-plugin": "5.3.0",
|
|
71
|
+
"@tailwindcss/postcss": "4.3.1",
|
|
72
|
+
"@vitejs/plugin-react": "6.0.2",
|
|
60
73
|
"antd": "6.3.1",
|
|
61
|
-
"dayjs": "1.11.
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"pino": "8.16.2",
|
|
74
|
+
"dayjs": "1.11.20",
|
|
75
|
+
"hono": "4.13.5",
|
|
76
|
+
"pino": "10.3.1",
|
|
65
77
|
"react": "18.2.0",
|
|
66
78
|
"react-dom": "18.2.0",
|
|
67
79
|
"react-icons": "5.6.0",
|
|
68
|
-
"tailwindcss": "4.
|
|
69
|
-
"uuid": "
|
|
80
|
+
"tailwindcss": "4.3.1",
|
|
81
|
+
"uuid": "14.0.0",
|
|
82
|
+
"vite": "8.0.16",
|
|
83
|
+
"ws": "8.21.0"
|
|
70
84
|
},
|
|
71
85
|
"devDependencies": {
|
|
72
|
-
"@lowdefy/build": "
|
|
73
|
-
"@
|
|
74
|
-
"
|
|
75
|
-
"tailwindcss": "4.
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"yargs": "17.7.2"
|
|
86
|
+
"@lowdefy/build": "6.1.0",
|
|
87
|
+
"@tailwindcss/postcss": "4.3.1",
|
|
88
|
+
"jest": "28.1.3",
|
|
89
|
+
"tailwindcss": "4.3.1",
|
|
90
|
+
"yaml": "2.9.0",
|
|
91
|
+
"yargs": "18.0.0"
|
|
79
92
|
},
|
|
80
93
|
"engines": {
|
|
81
|
-
"node": ">=
|
|
94
|
+
"node": ">=22"
|
|
82
95
|
},
|
|
83
96
|
"publishConfig": {
|
|
84
97
|
"access": "public"
|
|
85
98
|
},
|
|
99
|
+
"type": "module",
|
|
86
100
|
"scripts": {
|
|
87
101
|
"build": "cp package.json package.original.json || copy package.json package.original.json",
|
|
102
|
+
"build:client": "vite build",
|
|
88
103
|
"build:lowdefy": "node lowdefy/build.mjs",
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"start": "next start",
|
|
92
|
-
"lint": "next lint",
|
|
93
|
-
"next": "next"
|
|
104
|
+
"start": "node src/index.js",
|
|
105
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
94
106
|
}
|
|
95
107
|
}
|