@lowdefy/api 5.5.1 → 5.6.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/dist/index.js CHANGED
@@ -12,7 +12,8 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import callAgent from './routes/agent/callAgent.js';
15
+ */ import buildEndpointResult from './response/buildEndpointResult.js';
16
+ import callAgent from './routes/agent/callAgent.js';
16
17
  import callEndpoint from './routes/endpoints/callEndpoint.js';
17
18
  import callRequest from './routes/request/callRequest.js';
18
19
  import createApiContext from './context/createApiContext.js';
@@ -22,4 +23,6 @@ import getNextAuthConfig from './routes/auth/getNextAuthConfig.js';
22
23
  import getPageConfig from './routes/page/getPageConfig.js';
23
24
  import getRootConfig from './routes/rootConfig/getRootConfig.js';
24
25
  import logClientError from './routes/log/logClientError.js';
25
- export { callAgent, callEndpoint, callRequest, createApiContext, createSessionCallback, getHomeAndMenus, getNextAuthConfig, getPageConfig, getRootConfig, logClientError };
26
+ import redactErrorResponse from './response/redactErrorResponse.js';
27
+ import redactResponse from './response/redactResponse.js';
28
+ export { buildEndpointResult, callAgent, callEndpoint, callRequest, createApiContext, createSessionCallback, getHomeAndMenus, getNextAuthConfig, getPageConfig, getRootConfig, logClientError, redactErrorResponse, redactResponse };
@@ -0,0 +1,33 @@
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
+ */ import redactErrorResponse from './redactErrorResponse.js';
16
+ import redactResponse from './redactResponse.js';
17
+ // The wire object every endpoint route returns after running its routine. One
18
+ // function rather than a copy of the same return statement per route, so the
19
+ // `response` field cannot end up policed differently from the `error` field beside
20
+ // it - see redactResponse for why the response needs the policy at all.
21
+ function buildEndpointResult(context, { error, response, status }) {
22
+ const success = ![
23
+ 'error',
24
+ 'reject'
25
+ ].includes(status);
26
+ return {
27
+ error: redactErrorResponse(context, error),
28
+ response: redactResponse(context, response),
29
+ status: success ? 'success' : status,
30
+ success
31
+ };
32
+ }
33
+ export default buildEndpointResult;
@@ -0,0 +1,64 @@
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
+ */ import path from 'path';
16
+ import { type } from '@lowdefy/helpers';
17
+ // extractErrorProps emits a nested error as a plain props object, not as a second
18
+ // '~e' wrapper, so an error node is recognised by its shape. Keying on the shape
19
+ // rather than on a map of where errors can appear is deliberate: re-deriving those
20
+ // positions is the mistake the walk-level omit exists to avoid, and this predicate
21
+ // still visits every position.
22
+ //
23
+ // The check matters because the payload also carries author-written data the policy
24
+ // deliberately preserves - a UserError's non-Error cause, its metaData - and a
25
+ // `source` key inside those belongs to the app, not to us.
26
+ function isErrorNode(value) {
27
+ return type.isString(value.name) && type.isString(value.message);
28
+ }
29
+ // A prefix slice rather than path.relative or a parse of a `path:line` shape:
30
+ // source is `${resolvedPath}:${lineNumber}` only when a line number resolved and
31
+ // the bare path otherwise, and removing a prefix never touches the suffix, so both
32
+ // forms work without knowing which this is. Applied at every error node carrying a
33
+ // source, not only the outermost, because a strip is a no-op when the prefix is
34
+ // absent. Mutates the freshly serialized payload in place - nothing else holds it.
35
+ function stripConfigDirectory(value, prefix) {
36
+ if (type.isArray(value)) {
37
+ value.forEach((item)=>stripConfigDirectory(item, prefix));
38
+ return;
39
+ }
40
+ if (!type.isObject(value)) return;
41
+ if (isErrorNode(value) && type.isString(value.source) && value.source.startsWith(prefix)) {
42
+ value.source = value.source.slice(prefix.length);
43
+ }
44
+ Object.values(value).forEach((child)=>stripConfigDirectory(child, prefix));
45
+ }
46
+ // Guarantees `source` reaches a client config-relative, never as an absolute server
47
+ // path. resolveConfigLocation makes it absolute whenever the context carries a
48
+ // configDirectory - server-dev and server-e2e do, and production happens not to
49
+ // today by omission rather than by invariant, which is the drift this closes.
50
+ //
51
+ // A rewrite rather than an omission, and it needs the context, so it runs as a pass
52
+ // over the serialized payload instead of inside the walk.
53
+ function normalizeErrorSources(context, payload) {
54
+ // errorHandler has an else branch for requests with no lowdefyContext, so a
55
+ // missing context is a supported call - it only means no source to normalise.
56
+ const configDirectory = context?.configDirectory;
57
+ if (type.isNone(configDirectory)) return payload;
58
+ // configDirectory is `LOWDEFY_DIRECTORY_CONFIG || process.cwd()` at every site
59
+ // that sets it, so it may be relative or carry a trailing separator, while
60
+ // resolveConfigLocation built source with path.resolve. Compare normalised.
61
+ stripConfigDirectory(payload, `${path.resolve(configDirectory)}${path.sep}`);
62
+ return payload;
63
+ }
64
+ export default normalizeErrorSources;
@@ -0,0 +1,51 @@
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
+ */ import { UserError } from '@lowdefy/errors';
16
+ import { type } from '@lowdefy/helpers';
17
+ // The client-bound error policy: which fields of an error may cross the wire.
18
+ // Passed to serializer.serialize as `omitErrorProps`, so extractErrorProps applies
19
+ // it at EVERY error node the walk emits - the cause chain, an Error-valued own
20
+ // property, and an Error nested inside a plain object or array. The policy is
21
+ // stated against the emitter rather than against a response shape, so it cannot
22
+ // become depth-limited.
23
+ //
24
+ // `received` is not merely "may be sensitive": on the request path
25
+ // callRequestResolver sets it to the EVALUATED request properties, so a _secret
26
+ // resolved into a request header is in it. `stack` exposes server internals,
27
+ // including absolute node_modules paths. Both are unbounded runtime data nobody
28
+ // chose, in every environment - the full value stays in the server log, which is
29
+ // where a developer on their own machine reads it.
30
+ const ALWAYS_OMITTED = [
31
+ 'received',
32
+ 'stack'
33
+ ];
34
+ const OMITTED_WITH_CAUSE = [
35
+ ...ALWAYS_OMITTED,
36
+ 'cause'
37
+ ];
38
+ function omitErrorProps(error) {
39
+ // An Error cause is the trace the browser renders (name + message per level),
40
+ // so it is always kept - fields are taken from causes, causes are never pruned.
41
+ // A non-Error cause is internal server config: the whole endpoint routine, a
42
+ // control node, ajv errors. UserError is the exception, the one class whose
43
+ // payload the author wrote for the client.
44
+ //
45
+ // type.isError is `instanceof Error`, the same test extractErrorProps uses to
46
+ // decide which branch emits the cause - the two must agree.
47
+ if (type.isError(error.cause)) return ALWAYS_OMITTED;
48
+ if (error instanceof UserError) return ALWAYS_OMITTED;
49
+ return OMITTED_WITH_CAUSE;
50
+ }
51
+ export default omitErrorProps;
@@ -0,0 +1,30 @@
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
+ */ import { serializer, type } from '@lowdefy/helpers';
16
+ import normalizeErrorSources from './normalizeErrorSources.js';
17
+ import omitErrorProps from './omitErrorProps.js';
18
+ // Owns the serialization as well as the policy, so the policy cannot be forgotten:
19
+ // no bare serializer.serialize(error) is left in response position to wrap. Every
20
+ // route returning an error to a caller - any status, any transport - goes through
21
+ // here or through buildEndpointResult.
22
+ function redactErrorResponse(context, error) {
23
+ // Endpoint routes serialize the error field on success too, where it is null.
24
+ // Passing that through unchanged keeps them from emitting an empty {'~e'}.
25
+ if (type.isNone(error)) return error;
26
+ return normalizeErrorSources(context, serializer.serialize(error, {
27
+ omitErrorProps
28
+ }));
29
+ }
30
+ export default redactErrorResponse;
@@ -0,0 +1,31 @@
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
+ */ import { serializer } from '@lowdefy/helpers';
16
+ import normalizeErrorSources from './normalizeErrorSources.js';
17
+ import omitErrorProps from './omitErrorProps.js';
18
+ // The response-value call shape, beside redactErrorResponse's error-only one.
19
+ // A response is not an error, but makeReplacer wraps any Error it meets anywhere
20
+ // in a value, so a response holding one is an error-serialization site too - the
21
+ // grep that enumerated those sites could not see them, which is why this exists
22
+ // as a function rather than as a rule to remember.
23
+ //
24
+ // Same policy as the error field, because it reaches the same audience: a browser
25
+ // for a request or endpoint body, a third party for cron and detached.
26
+ function redactResponse(context, response) {
27
+ return normalizeErrorSources(context, serializer.serialize(response, {
28
+ omitErrorProps
29
+ }));
30
+ }
31
+ export default redactResponse;
@@ -0,0 +1,39 @@
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
+ */ import { ConfigError } from '@lowdefy/errors';
16
+ import { translate } from '@lowdefy/helpers';
17
+ function authorizeAgent({ authorize, i18n, logger }, { agentConfig }) {
18
+ if (!authorize(agentConfig)) {
19
+ logger.debug({
20
+ event: 'debug_agent_authorize',
21
+ authorized: false,
22
+ auth_config: agentConfig.auth
23
+ });
24
+ // Same message as an unknown agentId so responses do not reveal which agents exist.
25
+ throw new ConfigError(translate({
26
+ key: 'agent.runtime.agentNotFound',
27
+ values: {
28
+ agentId: agentConfig.agentId
29
+ },
30
+ i18n
31
+ }));
32
+ }
33
+ logger.debug({
34
+ event: 'debug_agent_authorize',
35
+ authorized: true,
36
+ auth_config: agentConfig.auth
37
+ });
38
+ }
39
+ export default authorizeAgent;
@@ -12,8 +12,10 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import { serializer, type } from '@lowdefy/helpers';
15
+ */ import { type } from '@lowdefy/helpers';
16
+ import buildEndpointResult from '../../response/buildEndpointResult.js';
16
17
  import createEvaluateOperators from '../../context/createEvaluateOperators.js';
18
+ import authorizeAgent from './authorizeAgent.js';
17
19
  import authorizeApiEndpoint from '../endpoints/authorizeApiEndpoint.js';
18
20
  import getEndpointConfig from '../endpoints/getEndpointConfig.js';
19
21
  import runRoutine from '../endpoints/runRoutine.js';
@@ -33,6 +35,9 @@ async function callAgent(context, { agentId, pageId, messages, conversationId, u
33
35
  const agentConfig = await getAgentConfig(context, {
34
36
  agentId
35
37
  });
38
+ authorizeAgent(context, {
39
+ agentConfig
40
+ });
36
41
  const agentContext = {
37
42
  conversationId: conversationId ?? undefined,
38
43
  pageId,
@@ -102,16 +107,11 @@ async function callAgent(context, { agentId, pageId, messages, conversationId, u
102
107
  const { error, response, status } = await runRoutine(context, routineContext, {
103
108
  routine: endpointConfig.routine
104
109
  });
105
- const success = ![
106
- 'error',
107
- 'reject'
108
- ].includes(status);
109
- return {
110
- error: serializer.serialize(error),
111
- response: serializer.serialize(response),
112
- status: success ? 'success' : status,
113
- success
114
- };
110
+ return buildEndpointResult(context, {
111
+ error,
112
+ response,
113
+ status
114
+ });
115
115
  },
116
116
  getEndpointConfig: async ({ endpointId })=>{
117
117
  return getEndpointConfig(context, {
@@ -119,9 +119,13 @@ async function callAgent(context, { agentId, pageId, messages, conversationId, u
119
119
  });
120
120
  },
121
121
  getAgentConfig: async ({ agentId })=>{
122
- return getAgentConfig(context, {
122
+ const subAgentConfig = await getAgentConfig(context, {
123
123
  agentId
124
124
  });
125
+ authorizeAgent(context, {
126
+ agentConfig: subAgentConfig
127
+ });
128
+ return subAgentConfig;
125
129
  },
126
130
  getConnectionForAgent: async ({ agentConfig: subAgentConfig })=>{
127
131
  const subConnectionConfig = await getConnectionConfig(context, {
@@ -12,12 +12,22 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import { set } from '@lowdefy/helpers';
15
+ */ import { ConfigError } from '@lowdefy/errors';
16
+ import { ReservedKeyError, set } from '@lowdefy/helpers';
16
17
  function addStepResult(context, routineContext, { result, stepId }) {
17
18
  const key = [
18
19
  stepId,
19
20
  ...routineContext.arrayIndices
20
21
  ].join('.');
21
- set(routineContext.steps, key, result);
22
+ try {
23
+ set(routineContext.steps, key, result);
24
+ } catch (error) {
25
+ if (error instanceof ReservedKeyError) {
26
+ throw new ConfigError(`Reserved step id "${error.segment}" cannot be used`, {
27
+ cause: error
28
+ });
29
+ }
30
+ throw error;
31
+ }
22
32
  }
23
33
  export default addStepResult;
@@ -15,6 +15,7 @@
15
15
  */ import { serializer } from '@lowdefy/helpers';
16
16
  import { ConfigError } from '@lowdefy/errors';
17
17
  import authorizeApiEndpoint from './authorizeApiEndpoint.js';
18
+ import buildEndpointResult from '../../response/buildEndpointResult.js';
18
19
  import createEvaluateOperators from '../../context/createEvaluateOperators.js';
19
20
  import getEndpointConfig from './getEndpointConfig.js';
20
21
  import runRoutine from './runRoutine.js';
@@ -59,15 +60,10 @@ async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
59
60
  const { error, response, status } = await runRoutine(context, routineContext, {
60
61
  routine: endpointConfig.routine
61
62
  });
62
- const success = ![
63
- 'error',
64
- 'reject'
65
- ].includes(status);
66
- return {
67
- error: serializer.serialize(error),
68
- response: serializer.serialize(response),
69
- status: success ? 'success' : status,
70
- success
71
- };
63
+ return buildEndpointResult(context, {
64
+ error,
65
+ response,
66
+ status
67
+ });
72
68
  }
73
69
  export default callEndpoint;
@@ -12,7 +12,8 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import { set } from '@lowdefy/helpers';
15
+ */ import { ConfigError } from '@lowdefy/errors';
16
+ import { ReservedKeyError, set } from '@lowdefy/helpers';
16
17
  function controlSetState(context, routineContext, { control }) {
17
18
  const { logger, evaluateOperators } = context;
18
19
  const { items } = routineContext;
@@ -30,7 +31,17 @@ function controlSetState(context, routineContext, { control }) {
30
31
  evaluated: evaluatedSetState
31
32
  });
32
33
  Object.entries(evaluatedSetState).forEach(([key, value])=>{
33
- set(routineContext.state, key, value);
34
+ try {
35
+ set(routineContext.state, key, value);
36
+ } catch (error) {
37
+ if (error instanceof ReservedKeyError) {
38
+ throw new ConfigError(`Reserved key "${error.segment}" cannot be used in :set_state`, {
39
+ cause: error,
40
+ configKey: control['~k']
41
+ });
42
+ }
43
+ throw error;
44
+ }
34
45
  });
35
46
  return {
36
47
  status: 'continue'
@@ -68,9 +68,11 @@ async function runRoutine(context, routineContext, { routine }) {
68
68
  error
69
69
  };
70
70
  }
71
+ // handleError sets error.handled once it has logged - it is the single sink
72
+ // that owns the flag, so a nested runRoutine re-throwing this error does not
73
+ // log it again.
71
74
  if (!error.handled) {
72
75
  await context.handleError(error);
73
- error.handled = true;
74
76
  }
75
77
  return {
76
78
  status: 'error',
@@ -24,6 +24,7 @@ import getRequestConfig from './getRequestConfig.js';
24
24
  import getRequestResolver from './getRequestResolver.js';
25
25
  import validateSchemas from './validateSchemas.js';
26
26
  import createEvaluateOperators from '../../context/createEvaluateOperators.js';
27
+ import redactResponse from '../../response/redactResponse.js';
27
28
  async function callRequest(context, { blockId, pageId, payload, requestId }) {
28
29
  const { logger } = context;
29
30
  context.blockId = blockId;
@@ -93,7 +94,7 @@ async function callRequest(context, { blockId, pageId, payload, requestId }) {
93
94
  id: requestConfig.id,
94
95
  success: true,
95
96
  type: requestConfig.type,
96
- response: serializer.serialize(response)
97
+ response: redactResponse(context, response)
97
98
  };
98
99
  }
99
100
  export default callRequest;
@@ -13,7 +13,7 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import createAuthorize from '../context/createAuthorize.js';
16
- function testContext({ appMeta = {}, config = {}, connections = {}, headers = {}, logger = {
16
+ function testContext({ appMeta = {}, config = {}, configDirectory, connections = {}, headers = {}, logger = {
17
17
  debug: ()=>{},
18
18
  error: ()=>{},
19
19
  info: ()=>{},
@@ -27,9 +27,14 @@ function testContext({ appMeta = {}, config = {}, connections = {}, headers = {}
27
27
  session
28
28
  }),
29
29
  config,
30
+ configDirectory,
30
31
  connections,
32
+ // Mirrors the servers' createHandleError contract: the sink logs the error
33
+ // and marks it handled, which is what runRoutine's guard and the client's
34
+ // already-logged check both read.
31
35
  handleError: async (error)=>{
32
36
  logger.error(error);
37
+ error.handled = true;
33
38
  },
34
39
  headers,
35
40
  logger,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/api",
3
- "version": "5.5.1",
3
+ "version": "5.6.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "",
6
6
  "homepage": "https://lowdefy.com",
@@ -34,13 +34,13 @@
34
34
  "dist/*"
35
35
  ],
36
36
  "dependencies": {
37
- "@lowdefy/ajv": "5.5.1",
38
- "@lowdefy/errors": "5.5.1",
39
- "@lowdefy/helpers": "5.5.1",
40
- "@lowdefy/node-utils": "5.5.1",
41
- "@lowdefy/nunjucks": "5.5.1",
42
- "@lowdefy/operators": "5.5.1",
43
- "@lowdefy/operators-js": "5.5.1"
37
+ "@lowdefy/ajv": "5.6.0",
38
+ "@lowdefy/errors": "5.6.0",
39
+ "@lowdefy/helpers": "5.6.0",
40
+ "@lowdefy/node-utils": "5.6.0",
41
+ "@lowdefy/nunjucks": "5.6.0",
42
+ "@lowdefy/operators": "5.6.0",
43
+ "@lowdefy/operators-js": "5.6.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@jest/globals": "28.1.3",