@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.
Files changed (79) hide show
  1. package/{pages/_app.js → client/App.jsx} +26 -34
  2. package/client/Page.jsx +122 -0
  3. package/client/clientBuildImports.test.mjs +61 -0
  4. package/client/main.jsx +39 -0
  5. package/client/shouldReloadForBuild.js +48 -0
  6. package/client/shouldReloadForBuild.test.mjs +75 -0
  7. package/lib/build/app.js +8 -1
  8. package/lib/build/appMeta.js +8 -1
  9. package/lib/build/auth.js +8 -1
  10. package/lib/build/config.js +8 -1
  11. package/lib/build/i18n.js +8 -1
  12. package/lib/build/logger.js +8 -1
  13. package/lib/build/theme.js +8 -1
  14. package/lib/client/auth/{Auth.js → Auth.jsx} +10 -2
  15. package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +21 -10
  16. package/lib/client/sentry/captureSentryError.js +1 -1
  17. package/lib/client/sentry/initSentryClient.js +1 -1
  18. package/lib/client/sentry/setSentryUser.js +1 -1
  19. package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +7 -4
  20. package/{pages/api/auth/[...nextauth].js → lib/server/auth/session.js} +9 -15
  21. package/lib/server/auth/strategies.js +47 -0
  22. package/lib/server/log/createHandleError.js +10 -5
  23. package/lib/server/log/createHandleError.test.mjs +85 -0
  24. package/lib/server/log/createLogger.js +28 -1
  25. package/lib/server/log/createLogger.test.mjs +102 -0
  26. package/lib/server/log/logRequest.js +47 -45
  27. package/lib/server/log/maskCredentialKeys.js +37 -0
  28. package/lib/server/log/projectErrorForLog.js +55 -0
  29. package/lib/server/log/redactUrlQuery.js +75 -0
  30. package/lib/server/log/redactUrlQuery.test.mjs +73 -0
  31. package/lib/server/{auth/getServerSession.js → log/serializeErrorForLog.js} +5 -8
  32. package/lib/server/log/serializeErrorForLog.test.mjs +325 -0
  33. package/lib/server/scrubSecrets.js +21 -0
  34. package/lib/server/sentry/captureSentryError.js +13 -1
  35. package/lib/server/sentry/captureSentryError.test.mjs +104 -0
  36. package/lib/server/sentry/initSentry.js +17 -4
  37. package/lib/server/sentry/initSentry.test.mjs +237 -0
  38. package/lib/server/sentry/scrubEvent.js +29 -0
  39. package/lib/server/sentry/scrubEvent.test.mjs +88 -0
  40. package/lib/server/sentry/setSentryUser.js +1 -1
  41. package/lowdefy/createCustomPluginTypesMap.mjs +1 -0
  42. package/package.json +54 -42
  43. package/package.original.json +56 -44
  44. package/src/app.js +140 -0
  45. package/src/html/getAssets.js +45 -0
  46. package/src/html/renderPage.js +80 -0
  47. package/src/html/template.js +95 -0
  48. package/src/index.js +95 -0
  49. package/src/lib/getPathSegments.js +25 -0
  50. package/src/lib/safeScriptJson.js +38 -0
  51. package/src/middleware/apiContext.js +138 -0
  52. package/src/middleware/apiContext.test.mjs +118 -0
  53. package/src/middleware/errorHandler.js +54 -0
  54. package/src/middleware/sentry.js +33 -0
  55. package/{pages/api/agent/[...path].js → src/routes/agent.js} +20 -42
  56. package/src/routes/apiPage.js +40 -0
  57. package/src/routes/apiPage.test.mjs +59 -0
  58. package/src/routes/auth.js +38 -0
  59. package/{pages/api/client-error.js → src/routes/clientError.js} +14 -16
  60. package/src/routes/cron.js +44 -0
  61. package/src/routes/cronForward.js +47 -0
  62. package/src/routes/detached.js +47 -0
  63. package/src/routes/endpoints.js +71 -0
  64. package/src/routes/mcp.js +34 -0
  65. package/{pages/api/request/[...path].js → src/routes/request.js} +10 -10
  66. package/src/routes/usage.js +46 -0
  67. package/src/routes/websocket.js +46 -0
  68. package/vite.config.js +61 -0
  69. package/.eslintrc.yaml +0 -1
  70. package/lib/client/Page.js +0 -56
  71. package/lib/server/apiWrapper.js +0 -87
  72. package/lib/server/serverSidePropsWrapper.js +0 -66
  73. package/next.config.js +0 -34
  74. package/pages/404.js +0 -49
  75. package/pages/[[...pageId]].js +0 -85
  76. package/pages/_document.js +0 -108
  77. package/pages/api/endpoints/[...endpointId].js +0 -32
  78. package/pages/api/usage.js +0 -67
  79. /package/{postcss.config.js → postcss.config.cjs} +0 -0
@@ -14,7 +14,7 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import * as Sentry from '@sentry/nextjs';
17
+ import * as Sentry from '@sentry/browser';
18
18
 
19
19
  let initialized = false;
20
20
 
@@ -14,7 +14,7 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import * as Sentry from '@sentry/nextjs';
17
+ import * as Sentry from '@sentry/browser';
18
18
 
19
19
  function setSentryUser({ user, sentryConfig }) {
20
20
  // No-op if no user
@@ -14,23 +14,26 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import { getNextAuthConfig } from '@lowdefy/api';
17
+ import { getAuthConfig as getApiAuthConfig } from '@lowdefy/api';
18
18
  import { getSecretsFromEnv } from '@lowdefy/node-utils';
19
19
 
20
20
  import adapters from '../../../build/plugins/auth/adapters.js';
21
+ import appMeta from '../../build/appMeta.js';
21
22
  import authJson from '../../build/auth.js';
23
+ import config from '../../build/config.js';
22
24
  import callbacks from '../../../build/plugins/auth/callbacks.js';
23
25
  import events from '../../../build/plugins/auth/events.js';
24
26
  import providers from '../../../build/plugins/auth/providers.js';
25
27
 
26
- function getAuthOptions({ appMeta, logger }) {
27
- return getNextAuthConfig({
28
+ function getAuthConfig({ logger }) {
29
+ return getApiAuthConfig({
28
30
  appMeta,
29
31
  authJson,
32
+ config,
30
33
  logger,
31
34
  plugins: { adapters, callbacks, events, providers },
32
35
  secrets: getSecretsFromEnv(),
33
36
  });
34
37
  }
35
38
 
36
- export default getAuthOptions;
39
+ export default getAuthConfig;
@@ -14,24 +14,18 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import NextAuth from 'next-auth';
17
+ import { getAuthUser } from '@hono/auth-js';
18
18
 
19
- import apiWrapper from '../../../lib/server/apiWrapper.js';
20
- import authJson from '../../../lib/build/auth.js';
19
+ import authJson from '../../build/auth.js';
21
20
 
22
- async function handler({ context, req, res }) {
21
+ // Replaces getServerSession.js — reads the session from the Hono context
22
+ // populated by the initAuthConfig middleware.
23
+ async function getSession(c) {
23
24
  if (authJson.configured !== true) {
24
- return res.status(404).json({
25
- message: 'Auth not configured',
26
- });
25
+ return undefined;
27
26
  }
28
-
29
- // Required for emails in corporate networks, see:
30
- // https://next-auth.js.org/tutorials/avoid-corporate-link-checking-email-provider
31
- if (req.method === 'HEAD') {
32
- return res.status(200).end();
33
- }
34
- return NextAuth(req, res, context.authOptions);
27
+ const authUser = await getAuthUser(c);
28
+ return authUser?.session ?? undefined;
35
29
  }
36
30
 
37
- export default apiWrapper(handler);
31
+ export default getSession;
@@ -0,0 +1,47 @@
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 { getAuthStrategies, resolveStrategyCaller } from '@lowdefy/api';
18
+ import { getSecretsFromEnv } from '@lowdefy/node-utils';
19
+
20
+ import appMeta from '../../build/appMeta.js';
21
+ import authJson from '../../build/auth.js';
22
+ import strategies from '../../../build/plugins/auth/strategies.js';
23
+
24
+ // Secrets are process-static env, so read once at module load rather than
25
+ // on every sessionless request.
26
+ const secrets = getSecretsFromEnv();
27
+
28
+ // API auth strategies (apiKey/jwt headers) let MCP and service clients that
29
+ // cannot hold a session cookie authenticate. Tried only when no session
30
+ // resolved; a match is synthesized as session.user so createAuthorize and
31
+ // _user work unchanged.
32
+ async function getStrategyCaller(c, logger) {
33
+ const authStrategies = getAuthStrategies({
34
+ appMeta,
35
+ authJson,
36
+ logger,
37
+ plugins: { strategies },
38
+ secrets,
39
+ });
40
+ return resolveStrategyCaller({
41
+ headers: c.req.raw.headers,
42
+ logger,
43
+ strategies: authStrategies,
44
+ });
45
+ }
46
+
47
+ export default getStrategyCaller;
@@ -27,6 +27,9 @@ import {
27
27
  } from '@lowdefy/errors';
28
28
 
29
29
  import captureSentryError from '../sentry/captureSentryError.js';
30
+ import redactUrlQuery from './redactUrlQuery.js';
31
+ import scrubSecrets from '../scrubSecrets.js';
32
+ import serializeErrorForLog from './serializeErrorForLog.js';
30
33
 
31
34
  function getEventType(error) {
32
35
  if (error instanceof ServiceError) {
@@ -92,7 +95,7 @@ function createHandleError({ context }) {
92
95
  error.config = location.config;
93
96
  }
94
97
 
95
- // Single structured log call — pino serializes error via extractErrorProps,
98
+ // Single structured log call — pino serializes the error via serializeErrorForLog,
96
99
  // message string gives human-readable display via CLI logger
97
100
  const errorName = error?.name || 'Error';
98
101
  context.logger.error(
@@ -125,9 +128,9 @@ function createHandleError({ context }) {
125
128
  'sec-ch-ua': headers['sec-ch-ua'],
126
129
  'user-agent': headers['user-agent'],
127
130
  host: headers.host,
128
- referer: headers.referer,
131
+ referer: redactUrlQuery(headers.referer),
129
132
  // Non localhost headers
130
- 'x-forward-for': headers['x-forward-for'],
133
+ 'x-forwarded-for': headers['x-forwarded-for'],
131
134
  // Vercel headers
132
135
  'x-vercel-id': headers['x-vercel-id'],
133
136
  'x-real-ip': headers['x-real-ip'],
@@ -154,9 +157,11 @@ function createHandleError({ context }) {
154
157
  configLocation: location,
155
158
  });
156
159
  } catch (e) {
157
- console.error(error);
160
+ // console.error writes to stderr past pino and its line scrub, and would print the raw
161
+ // error - an axios error's config.auth included - so it gets both layers by hand.
162
+ console.error(scrubSecrets(JSON.stringify(serializeErrorForLog(error))));
158
163
  console.error('An error occurred while logging the error.');
159
- console.error(e);
164
+ console.error(scrubSecrets(JSON.stringify(serializeErrorForLog(e))));
160
165
  }
161
166
  };
162
167
  }
@@ -0,0 +1,85 @@
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
+
19
+ const secret = 'planted-handle-error-secret';
20
+
21
+ process.env.LOWDEFY_SECRET_API_KEY = secret;
22
+
23
+ const { default: createHandleError } = await import('./createHandleError.js');
24
+
25
+ function createAxiosError() {
26
+ const error = new Error(`Request failed for key ${secret}`);
27
+ error.name = 'AxiosError';
28
+ error.code = 'ERR_BAD_REQUEST';
29
+ error.config = {
30
+ auth: { username: 'svc-user', password: 'planted-config-password' },
31
+ params: { api_key: 'planted-config-param' },
32
+ headers: { Authorization: 'Bearer planted-config-header' },
33
+ };
34
+ return error;
35
+ }
36
+
37
+ function testContext({ logger, headers } = {}) {
38
+ return {
39
+ headers,
40
+ logger: logger ?? { error: jest.fn() },
41
+ readConfigFile: jest.fn(async () => null),
42
+ req: { url: '/api/request/page/req', method: 'POST', hostname: 'app.example.com' },
43
+ };
44
+ }
45
+
46
+ beforeEach(() => {
47
+ console.error = jest.fn();
48
+ });
49
+
50
+ test('handleError logs the referer header with credential query parameters redacted', async () => {
51
+ const context = testContext({
52
+ headers: { referer: 'https://app.example.com/verify?token=planted-token&page=2' },
53
+ });
54
+ const handleError = createHandleError({ context });
55
+
56
+ await handleError(new Error('Failed.'));
57
+
58
+ const [fields] = context.logger.error.mock.calls[0];
59
+ expect(fields.headers.referer).toBe('https://app.example.com/verify?token=%5Bredacted%5D&page=2');
60
+ });
61
+
62
+ test('handleError fallback output carries no axios config auth and no planted secret when the log call throws', async () => {
63
+ const logError = new Error(`Log sink rejected ${secret}`);
64
+ const context = testContext({
65
+ logger: {
66
+ error: jest.fn(() => {
67
+ throw logError;
68
+ }),
69
+ },
70
+ });
71
+ const handleError = createHandleError({ context });
72
+
73
+ await handleError(createAxiosError());
74
+
75
+ expect(console.error).toHaveBeenCalledTimes(3);
76
+ const output = console.error.mock.calls.map(([line]) => line).join('\n');
77
+ expect(output).not.toContain('planted-config-password');
78
+ expect(output).not.toContain('planted-config-param');
79
+ expect(output).not.toContain('planted-config-header');
80
+ expect(output).not.toContain(secret);
81
+ const logged = JSON.parse(console.error.mock.calls[0][0]);
82
+ expect(logged.message).toBe('Request failed for key [REDACTED]');
83
+ expect(logged.code).toBe('ERR_BAD_REQUEST');
84
+ expect(JSON.parse(console.error.mock.calls[2][0]).message).toBe('Log sink rejected [REDACTED]');
85
+ });
@@ -15,10 +15,37 @@
15
15
  */
16
16
 
17
17
  import { createNodeLogger } from '@lowdefy/logger/node';
18
+ import { type } from '@lowdefy/helpers';
19
+
20
+ import appMeta from '../../build/appMeta.js';
21
+ import scrubSecrets from '../scrubSecrets.js';
22
+ import serializeErrorForLog from './serializeErrorForLog.js';
23
+
24
+ // Deploy identity on every log line — pid/hostname are stripped, so these
25
+ // fields are what correlates a line to an app build across replicas.
26
+ const base = { pid: undefined, hostname: undefined };
27
+ if (!type.isNone(appMeta.name)) {
28
+ base.app_name = appMeta.name;
29
+ }
30
+ if (!type.isNone(appMeta.version)) {
31
+ base.app_version = appMeta.version;
32
+ }
33
+ if (!type.isNone(appMeta.gitSha)) {
34
+ base.git_sha = appMeta.gitSha;
35
+ }
36
+ // The deployment environment (LOWDEFY_ENVIRONMENT), so one log drain can hold every environment.
37
+ if (!type.isNone(appMeta.environment)) {
38
+ base.environment = appMeta.environment;
39
+ }
40
+
18
41
  const logger = createNodeLogger({
19
42
  name: 'lowdefy_server',
20
43
  level: process.env.LOWDEFY_LOG_LEVEL ?? 'info',
21
- base: { pid: undefined, hostname: undefined },
44
+ base,
45
+ serializers: { err: serializeErrorForLog },
46
+ // Scrubbing the finished line rather than the logged object covers every field of every
47
+ // call - debug payloads and request results as well as `err` - with no walker to miss one.
48
+ hooks: { streamWrite: scrubSecrets },
22
49
  });
23
50
 
24
51
  function createLogger(metadata = {}) {
@@ -0,0 +1,102 @@
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
+
20
+ const secret = 'planted/secret+value=1';
21
+ const shortSecret = 'short77';
22
+
23
+ const lines = [];
24
+ const destination = {
25
+ write(line) {
26
+ lines.push(line);
27
+ },
28
+ };
29
+
30
+ // The prod logger writes to stdout; route the real logger to memory so each test reads the
31
+ // exact line pino would emit, after the streamWrite hook.
32
+ jest.unstable_mockModule('@lowdefy/logger/node', () => ({
33
+ createNodeLogger: (options) => createNodeLogger({ ...options, destination }),
34
+ }));
35
+ jest.unstable_mockModule('../../build/appMeta.js', () => ({ default: {} }));
36
+
37
+ process.env.LOWDEFY_SECRET_API_KEY = secret;
38
+ process.env.LOWDEFY_SECRET_SHORT = shortSecret;
39
+ process.env.LOWDEFY_LOG_LEVEL = 'debug';
40
+
41
+ const { default: createLogger } = await import('./createLogger.js');
42
+
43
+ function createAxiosError() {
44
+ const error = new Error('Request failed with status code 401');
45
+ error.name = 'AxiosError';
46
+ error.code = 'ERR_BAD_REQUEST';
47
+ error.config = {
48
+ auth: { username: 'svc-user', password: 'planted-config-password' },
49
+ params: { api_key: 'planted-config-param' },
50
+ headers: { Authorization: 'Bearer planted-config-header' },
51
+ baseURL: 'https://api.example.com',
52
+ };
53
+ error.response = { status: 401, data: { detail: 'planted-response-body' } };
54
+ return error;
55
+ }
56
+
57
+ beforeEach(() => {
58
+ lines.length = 0;
59
+ });
60
+
61
+ test('prod logger drops an axios error config auth, params and headers', () => {
62
+ createLogger().error({ err: createAxiosError() }, 'Request failed');
63
+ expect(lines).toHaveLength(1);
64
+ const line = lines[0];
65
+ expect(line).not.toContain('planted-config-password');
66
+ expect(line).not.toContain('planted-config-param');
67
+ expect(line).not.toContain('planted-config-header');
68
+ expect(line).not.toContain('planted-response-body');
69
+ const { err } = JSON.parse(line);
70
+ expect(err.config).toBeUndefined();
71
+ expect(err.code).toBe('ERR_BAD_REQUEST');
72
+ expect(err.statusCode).toBe(401);
73
+ });
74
+
75
+ test('prod logger redacts a planted secret in a message raw, URL-encoded and base64', () => {
76
+ const basic = Buffer.from(`svc-user:${secret}`).toString('base64');
77
+ const error = new Error(`raw ${secret} encoded ${encodeURIComponent(secret)} basic ${basic}`);
78
+ createLogger().error({ err: error }, error.message);
79
+ expect(lines).toHaveLength(1);
80
+ const line = lines[0];
81
+ expect(line).not.toContain(secret);
82
+ expect(line).not.toContain(encodeURIComponent(secret));
83
+ expect(line).not.toContain(Buffer.from(secret).toString('base64').slice(0, 20));
84
+ const { msg, err } = JSON.parse(line);
85
+ expect(msg).toMatch(/^raw \[REDACTED\] encoded \[REDACTED\] basic \S*\[REDACTED\]/);
86
+ expect(err.message).toBe(msg);
87
+ });
88
+
89
+ test('prod logger redacts a planted secret in a debug payload that is not an error', () => {
90
+ createLogger({ rid: 'req-1' }).debug({ response: { token: secret } }, 'Request result');
91
+ expect(lines).toHaveLength(1);
92
+ expect(lines[0]).not.toContain(secret);
93
+ expect(JSON.parse(lines[0]).response.token).toBe('[REDACTED]');
94
+ });
95
+
96
+ test('prod logger leaves a secret shorter than 8 characters alone', () => {
97
+ createLogger().info({ detail: shortSecret }, `value ${shortSecret}`);
98
+ expect(lines).toHaveLength(1);
99
+ const { msg, detail } = JSON.parse(lines[0]);
100
+ expect(msg).toBe(`value ${shortSecret}`);
101
+ expect(detail).toBe(shortSecret);
102
+ });
@@ -14,53 +14,55 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- // TODO: Better name needed here maybe?
18
- function logRequest({ context }) {
17
+ import redactUrlQuery from './redactUrlQuery.js';
18
+
19
+ // Emits one access-log line per request, after the response is finalized.
20
+ // `url` is the route path only (c.req.path). The referer keeps its query but
21
+ // with credential parameters redacted, because the browser sends the page's
22
+ // full URL there and that URL can carry a sign-in token or a signed OAuth
23
+ // query (see redactUrlQuery).
24
+ function logRequest({ context, status, durationMs }) {
19
25
  const { headers = {}, user = {} } = context;
20
- context.logger.info({
21
- // TODO:
22
- // app_name
23
- // app_version
24
- // lowdefy_version
25
- // build_hash
26
- // config_hash
27
- user: {
28
- id: user.id,
29
- roles: user.roles,
30
- sub: user.sub,
31
- session_id: user.session_id, // TODO: Implement session id
32
- },
33
- url: context.req.url,
34
- method: context.req.method,
35
- resolvedUrl: context.nextContext?.resolvedUrl,
36
- hostname: context.req.hostname,
37
- headers: {
38
- 'accept-language': headers['accept-language'],
39
- 'sec-ch-ua-mobile': headers['sec-ch-ua-mobile'],
40
- 'sec-ch-ua-platform': headers['sec-ch-ua-platform'],
41
- 'sec-ch-ua': headers['sec-ch-ua'],
42
- 'user-agent': headers['user-agent'],
43
- host: headers.host,
44
- referer: headers.referer,
45
- 'x-forward-for': headers['x-forward-for'],
46
- // Vercel headers
47
- 'x-vercel-id': headers['x-vercel-id'],
48
- 'x-real-ip': headers['x-real-ip'],
49
- 'x-vercel-ip-country': headers['x-vercel-ip-country'],
50
- 'x-vercel-ip-country-region': headers['x-vercel-ip-country-region'],
51
- 'x-vercel-ip-city': headers['x-vercel-ip-city'],
52
- 'x-vercel-ip-latitude': headers['x-vercel-ip-latitude'],
53
- 'x-vercel-ip-longitude': headers['x-vercel-ip-longitude'],
54
- 'x-vercel-ip-timezone': headers['x-vercel-ip-timezone'],
55
- // Cloudflare headers
56
- 'cf-connecting-ip': headers['cf-connecting-ip'],
57
- 'cf-ray': headers['cf-ray'],
58
- 'cf-ipcountry': headers['cf-ipcountry'],
59
- 'cf-visitor': headers['cf-visitor'],
26
+ context.logger.info(
27
+ {
28
+ user: {
29
+ id: user.id,
30
+ roles: user.roles,
31
+ sub: user.sub,
32
+ session_id: user.session_id, // TODO: Implement session id
33
+ },
34
+ url: context.req.url,
35
+ method: context.req.method,
36
+ hostname: context.req.hostname,
37
+ status,
38
+ duration_ms: durationMs,
39
+ headers: {
40
+ 'accept-language': headers['accept-language'],
41
+ 'sec-ch-ua-mobile': headers['sec-ch-ua-mobile'],
42
+ 'sec-ch-ua-platform': headers['sec-ch-ua-platform'],
43
+ 'sec-ch-ua': headers['sec-ch-ua'],
44
+ 'user-agent': headers['user-agent'],
45
+ host: headers.host,
46
+ referer: redactUrlQuery(headers.referer),
47
+ 'x-forwarded-for': headers['x-forwarded-for'],
48
+ // Vercel headers
49
+ 'x-vercel-id': headers['x-vercel-id'],
50
+ 'x-real-ip': headers['x-real-ip'],
51
+ 'x-vercel-ip-country': headers['x-vercel-ip-country'],
52
+ 'x-vercel-ip-country-region': headers['x-vercel-ip-country-region'],
53
+ 'x-vercel-ip-city': headers['x-vercel-ip-city'],
54
+ 'x-vercel-ip-latitude': headers['x-vercel-ip-latitude'],
55
+ 'x-vercel-ip-longitude': headers['x-vercel-ip-longitude'],
56
+ 'x-vercel-ip-timezone': headers['x-vercel-ip-timezone'],
57
+ // Cloudflare headers
58
+ 'cf-connecting-ip': headers['cf-connecting-ip'],
59
+ 'cf-ray': headers['cf-ray'],
60
+ 'cf-ipcountry': headers['cf-ipcountry'],
61
+ 'cf-visitor': headers['cf-visitor'],
62
+ },
60
63
  },
61
- });
62
- // TODO:
63
- // Next local? nextContext.locale, nextContext.locales, nextContext.defaultLocale
64
+ 'request completed'
65
+ );
64
66
  }
65
67
 
66
68
  export default logRequest;
@@ -0,0 +1,37 @@
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 } from '@lowdefy/helpers';
18
+
19
+ // `received` is the one kept log field holding values fetched at runtime (a token
20
+ // from an earlier step sent as a header), which the by-value secret scrub
21
+ // cannot recognise, so credential-looking keys are masked by name instead.
22
+ const credentialKeyParts = ['authorization', 'token', 'secret', 'password', 'apikey', 'cookie'];
23
+
24
+ function isCredentialKey(key) {
25
+ const normalized = key.toLowerCase().replace(/[-_]/g, '');
26
+ return credentialKeyParts.some((part) => normalized.includes(part));
27
+ }
28
+
29
+ // Shared references and cycles are copied as such, so the serializer walk still
30
+ // marks them '[Circular]'.
31
+ function maskCredentialKeys(value) {
32
+ return mapPlainValues(value, (item, key) =>
33
+ key !== undefined && isCredentialKey(key) ? '[REDACTED]' : item
34
+ );
35
+ }
36
+
37
+ export default maskCredentialKeys;
@@ -0,0 +1,55 @@
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 { lowdefyErrorNames, readErrorCodes } from '@lowdefy/errors';
18
+
19
+ import maskCredentialKeys from './maskCredentialKeys.js';
20
+
21
+ // Kept only on nodes named as a Lowdefy error class: libraries reuse some of
22
+ // these names for their own data - axios's `config` carries auth, params and
23
+ // headers - so keying on the field alone would let a library's copy through.
24
+ const lowdefyErrorFields = [
25
+ 'configKey',
26
+ 'source',
27
+ 'config',
28
+ 'handled',
29
+ 'isLowdefyError',
30
+ 'received',
31
+ 'typeName',
32
+ 'location',
33
+ 'service',
34
+ 'hint',
35
+ 'methodName',
36
+ 'metaData',
37
+ 'blockId',
38
+ 'pageId',
39
+ 'isReject',
40
+ ];
41
+
42
+ function projectErrorForLog(err) {
43
+ const { code, statusCode } = readErrorCodes(err);
44
+ const props = { name: err.name, message: err.message, stack: err.stack, code, statusCode };
45
+ if (lowdefyErrorNames.has(err.name)) {
46
+ lowdefyErrorFields.forEach((field) => {
47
+ props[field] = err[field];
48
+ });
49
+ props.received = maskCredentialKeys(err.received);
50
+ }
51
+ props.cause = err.cause;
52
+ return props;
53
+ }
54
+
55
+ export default projectErrorForLog;