@lowdefy/api 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 (72) hide show
  1. package/dist/context/createAuthorize.js +4 -1
  2. package/dist/context/createEvaluateOperators.js +3 -1
  3. package/dist/context/getCurrentEnvironment.js +27 -0
  4. package/dist/context/resolveStrategyCaller.js +56 -0
  5. package/dist/index.js +13 -2
  6. package/dist/response/buildEndpointResult.js +3 -1
  7. package/dist/response/createWireProjection.js +88 -0
  8. package/dist/response/normalizeErrorSources.js +10 -13
  9. package/dist/response/redactErrorResponse.js +42 -8
  10. package/dist/response/redactResponse.js +10 -10
  11. package/dist/routes/agent/callAgent.js +7 -184
  12. package/dist/routes/agent/prepareAgent.js +179 -0
  13. package/dist/routes/auth/createLogger.js +2 -5
  14. package/dist/routes/auth/createPrefixedCookies.js +52 -0
  15. package/dist/routes/auth/defaultAuthUrl.js +31 -0
  16. package/dist/routes/auth/{getNextAuthConfig.js → getAuthConfig.js} +31 -16
  17. package/dist/routes/auth/resolveCookies.js +37 -0
  18. package/dist/routes/auth/strategies/createAuthStrategies.js +77 -0
  19. package/dist/routes/auth/strategies/getAuthStrategies.js +31 -0
  20. package/dist/routes/endpoints/authorizeApiEndpoint.js +8 -2
  21. package/dist/routes/endpoints/callEndpoint.js +27 -3
  22. package/dist/routes/endpoints/control/controlFor.js +4 -7
  23. package/dist/routes/endpoints/control/controlIf.js +4 -8
  24. package/dist/routes/endpoints/control/controlLog.js +6 -14
  25. package/dist/routes/endpoints/control/controlParallelFor.js +4 -7
  26. package/dist/routes/endpoints/control/controlReject.js +16 -16
  27. package/dist/routes/endpoints/control/controlReturn.js +4 -9
  28. package/dist/routes/endpoints/control/controlSetState.js +4 -8
  29. package/dist/routes/endpoints/control/controlSwitch.js +4 -8
  30. package/dist/routes/endpoints/control/controlThrow.js +21 -20
  31. package/dist/routes/endpoints/control/controlTry.js +20 -10
  32. package/dist/routes/endpoints/evaluateRoutineOperators.js +26 -0
  33. package/dist/routes/endpoints/findSchedule.js +35 -0
  34. package/dist/routes/endpoints/forwardScheduledEndpoint.js +111 -0
  35. package/dist/routes/endpoints/getEndpointConfig.js +9 -3
  36. package/dist/routes/endpoints/getEnvironmentSchedules.js +27 -0
  37. package/dist/routes/endpoints/handleAgentCall.js +80 -0
  38. package/dist/routes/endpoints/handleEndpointCall.js +53 -8
  39. package/dist/routes/endpoints/handleRenderNotification.js +196 -0
  40. package/dist/routes/endpoints/handleRequest.js +1 -5
  41. package/dist/routes/endpoints/handleValidateSchema.js +14 -10
  42. package/dist/routes/endpoints/invokeEndpoint.js +1 -0
  43. package/dist/routes/endpoints/isUnauthenticatedHuman.js +31 -0
  44. package/dist/routes/endpoints/operatorScope.js +27 -0
  45. package/dist/routes/endpoints/resolveCronEnvironment.js +38 -0
  46. package/dist/routes/endpoints/runDetachedEndpoint.js +67 -0
  47. package/dist/routes/endpoints/runRoutine.js +12 -0
  48. package/dist/routes/endpoints/runScheduledEndpoint.js +105 -0
  49. package/dist/routes/endpoints/runWebhookEndpoint.js +84 -0
  50. package/dist/routes/endpoints/scheduleBackground.js +48 -0
  51. package/dist/routes/endpoints/test/runTest.js +17 -2
  52. package/dist/routes/mcp/createMcpServer.js +178 -0
  53. package/dist/routes/notifications/derivePreview.js +30 -0
  54. package/dist/routes/notifications/getNotificationConfig.js +32 -0
  55. package/dist/routes/notifications/resolveNotificationLinks.js +72 -0
  56. package/dist/routes/notifications/resolveThemeLogo.js +35 -0
  57. package/dist/routes/page/dynamic/resolveDynamicContent.js +172 -0
  58. package/dist/routes/page/dynamic/unescapeOperators.js +36 -0
  59. package/dist/routes/page/dynamic/validateFragment.js +104 -0
  60. package/dist/routes/page/getPageConfig.js +16 -6
  61. package/dist/routes/request/callRequest.js +9 -3
  62. package/dist/routes/request/callRequestResolver.js +6 -0
  63. package/dist/routes/request/evaluateOperators.js +8 -12
  64. package/dist/routes/websocket/authorizeWebsocket.js +27 -0
  65. package/dist/routes/websocket/createChannelRegistry.js +283 -0
  66. package/dist/routes/websocket/createWebSocketConnection.js +143 -0
  67. package/dist/routes/websocket/getWebsocketConfig.js +30 -0
  68. package/dist/routes/websocket/getWebsocketResolver.js +33 -0
  69. package/dist/routes/websocket/prepareChannel.js +72 -0
  70. package/dist/test/testContext.js +7 -3
  71. package/package.json +12 -10
  72. package/dist/response/omitErrorProps.js +0 -51
@@ -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 { ConfigError } from '@lowdefy/errors';
16
- function createAuthorize({ session }) {
16
+ function createAuthorize({ session, system = false }) {
17
17
  // Next-auth getSession provides a session object if the user is authenticated
18
18
  // else session will be null
19
19
  const authenticated = !!session;
@@ -24,6 +24,9 @@ function createAuthorize({ session }) {
24
24
  });
25
25
  }
26
26
  function authorize(config) {
27
+ // A system context (scheduled, webhook, detached runs) was authorized at the
28
+ // transport layer, so nested endpoint calls are never gated on a user session.
29
+ if (system === true) return true;
27
30
  const { auth } = config;
28
31
  if (auth.public === true) return true;
29
32
  if (auth.public === false) {
@@ -23,8 +23,10 @@ function createEvaluateOperators(context) {
23
23
  secrets,
24
24
  user
25
25
  });
26
- function evaluateOperators({ input, items, location, payload, state, steps }) {
26
+ function evaluateOperators({ arrayIndices, error, input, items, location, payload, state, steps }) {
27
27
  const { output, errors } = operatorsParser.parse({
28
+ arrayIndices,
29
+ error,
28
30
  input,
29
31
  items,
30
32
  location,
@@ -0,0 +1,27 @@
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
+ */ // The environment this deployment is, resolved by the build from LOWDEFY_ENVIRONMENT into
16
+ // config.environment and config.environments. Null when no current environment is set or when it
17
+ // is not declared, so callers fall back to their own defaults.
18
+ function getCurrentEnvironment({ config }) {
19
+ const name = config?.environment;
20
+ const settings = config?.environments?.[name];
21
+ if (settings === undefined) return null;
22
+ return {
23
+ ...settings,
24
+ name
25
+ };
26
+ }
27
+ export default getCurrentEnvironment;
@@ -0,0 +1,56 @@
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 { type } from '@lowdefy/helpers';
16
+ // Tries the configured API auth strategies in config order; the first
17
+ // verifier match wins. A strategy caller is a config-derived, org-less
18
+ // principal: roles are the strategy's static grant unioned with any
19
+ // claim-derived roles, and attributes are the static bag shallow-merged
20
+ // with claim-mapped values (claim wins), so _user.attributes reads the same
21
+ // for session and strategy callers.
22
+ async function resolveStrategyCaller({ headers, logger, strategies }) {
23
+ for (const strategy of strategies ?? []){
24
+ const match = await strategy.verify({
25
+ headers,
26
+ logger
27
+ });
28
+ if (type.isNone(match)) {
29
+ continue;
30
+ }
31
+ const roles = [
32
+ ...new Set([
33
+ ...strategy.roles,
34
+ ...match.roles ?? []
35
+ ])
36
+ ];
37
+ const attributes = {
38
+ ...strategy.attributes,
39
+ ...match.attributes ?? {}
40
+ };
41
+ logger.debug({
42
+ event: 'auth_strategy_authenticated',
43
+ authMethod: strategy.type,
44
+ strategyId: strategy.id
45
+ }, `Request authenticated by auth strategy "${strategy.id}" (${strategy.type}).`);
46
+ return {
47
+ ...match.user,
48
+ authMethod: strategy.type,
49
+ strategyId: strategy.id,
50
+ roles,
51
+ attributes
52
+ };
53
+ }
54
+ return null;
55
+ }
56
+ export default resolveStrategyCaller;
package/dist/index.js CHANGED
@@ -15,14 +15,25 @@
15
15
  */ import buildEndpointResult from './response/buildEndpointResult.js';
16
16
  import callAgent from './routes/agent/callAgent.js';
17
17
  import callEndpoint from './routes/endpoints/callEndpoint.js';
18
+ import getEndpointConfig from './routes/endpoints/getEndpointConfig.js';
18
19
  import callRequest from './routes/request/callRequest.js';
19
20
  import createApiContext from './context/createApiContext.js';
21
+ import createChannelRegistry from './routes/websocket/createChannelRegistry.js';
22
+ import createMcpServer from './routes/mcp/createMcpServer.js';
23
+ import createWebSocketConnection from './routes/websocket/createWebSocketConnection.js';
24
+ import createWireProjection from './response/createWireProjection.js';
20
25
  import createSessionCallback from './routes/auth/callbacks/createSessionCallback.js';
26
+ import getAuthConfig from './routes/auth/getAuthConfig.js';
27
+ import getAuthStrategies from './routes/auth/strategies/getAuthStrategies.js';
21
28
  import getHomeAndMenus from './routes/rootConfig/getHomeAndMenus.js';
22
- import getNextAuthConfig from './routes/auth/getNextAuthConfig.js';
29
+ import resolveStrategyCaller from './context/resolveStrategyCaller.js';
23
30
  import getPageConfig from './routes/page/getPageConfig.js';
24
31
  import getRootConfig from './routes/rootConfig/getRootConfig.js';
25
32
  import logClientError from './routes/log/logClientError.js';
26
33
  import redactErrorResponse from './response/redactErrorResponse.js';
27
34
  import redactResponse from './response/redactResponse.js';
28
- export { buildEndpointResult, callAgent, callEndpoint, callRequest, createApiContext, createSessionCallback, getHomeAndMenus, getNextAuthConfig, getPageConfig, getRootConfig, logClientError, redactErrorResponse, redactResponse };
35
+ import forwardScheduledEndpoint from './routes/endpoints/forwardScheduledEndpoint.js';
36
+ import runDetachedEndpoint from './routes/endpoints/runDetachedEndpoint.js';
37
+ import runWebhookEndpoint from './routes/endpoints/runWebhookEndpoint.js';
38
+ import runScheduledEndpoint from './routes/endpoints/runScheduledEndpoint.js';
39
+ export { buildEndpointResult, callAgent, callEndpoint, getEndpointConfig, callRequest, createApiContext, createChannelRegistry, createMcpServer, createSessionCallback, createWebSocketConnection, createWireProjection, getAuthConfig, getAuthStrategies, getHomeAndMenus, getPageConfig, getRootConfig, logClientError, redactErrorResponse, redactResponse, resolveStrategyCaller, forwardScheduledEndpoint, runDetachedEndpoint, runWebhookEndpoint, runScheduledEndpoint };
@@ -17,7 +17,9 @@ import redactResponse from './redactResponse.js';
17
17
  // The wire object every endpoint route returns after running its routine. One
18
18
  // function rather than a copy of the same return statement per route, so the
19
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.
20
+ // it - see redactResponse for why the response needs the policy at all. `error` is
21
+ // redactErrorResponse's error payload, so in dev it carries `devError`; `response`
22
+ // never does.
21
23
  function buildEndpointResult(context, { error, response, status }) {
22
24
  const success = ![
23
25
  'error',
@@ -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
+ */ import { lowdefyErrorNames, readErrorCodes } from '@lowdefy/errors';
16
+ import { translate } from '@lowdefy/helpers';
17
+ // The framework writes these messages to reveal nothing, and clients branch on
18
+ // them, so they keep their text where every other non-author error does not.
19
+ const AUTH_REFUSAL_NAMES = new Set([
20
+ 'AuthenticationError',
21
+ 'AuthorizationError',
22
+ 'TwoFactorEnrolmentRequiredError'
23
+ ]);
24
+ // The client-bound error policy, as an allowlist. Passed to serializer.serialize
25
+ // as `projectError`, so it runs at every error node the walk emits - the cause
26
+ // chain, an Error-valued own property, an Error nested in a response value - and
27
+ // cannot become depth-limited.
28
+ //
29
+ // Only the author's message crosses. A library's message embeds whatever the
30
+ // library saw - URLs with credentials, SQL, hostnames - so every other error
31
+ // becomes one translated generic message plus the fields client config branches
32
+ // on. The full error stays in the server log, and in dev reaches the developer
33
+ // through `devError`, which config never reads.
34
+ //
35
+ // The context is optional: a websocket broadcast has no single request, so it
36
+ // passes no `rid`, and the server error handler can run before any request
37
+ // context exists.
38
+ function createWireProjection(context) {
39
+ const requestId = context?.rid;
40
+ const genericMessage = translate({
41
+ key: 'server.genericError',
42
+ i18n: context?.i18n
43
+ });
44
+ return function project(err) {
45
+ // A PluginError defaults configKey to null; the walk only drops undefined.
46
+ const configKey = err.configKey ?? undefined;
47
+ // Keyed on the name rather than instanceof: the error may come from another
48
+ // copy of @lowdefy/errors or from a serializer round trip, where the class
49
+ // identity is gone but the name survives. The cause is returned raw so the
50
+ // walk projects it too - an Error cause takes the generic shape, and a
51
+ // non-Error cause is author data cleaned as a value.
52
+ if (err.name === 'UserError') {
53
+ return {
54
+ name: err.name,
55
+ message: err.message,
56
+ cause: err.cause,
57
+ metaData: err.metaData,
58
+ blockId: err.blockId,
59
+ pageId: err.pageId,
60
+ isReject: err.isReject,
61
+ configKey,
62
+ // Not read from the node: a caught error rebuilt for `_error` keeps only
63
+ // its class, and without the flag the client wraps a rethrown UserError
64
+ // in an ActionError and reports it back to the server a second time.
65
+ isLowdefyError: true,
66
+ handled: err.handled,
67
+ requestId
68
+ };
69
+ }
70
+ const { code, statusCode } = readErrorCodes(err);
71
+ return {
72
+ // A foreign class name can itself say which library or driver failed.
73
+ name: lowdefyErrorNames.has(err.name) ? err.name : 'Error',
74
+ message: AUTH_REFUSAL_NAMES.has(err.name) ? err.message : genericMessage,
75
+ code,
76
+ statusCode,
77
+ configKey,
78
+ requestId,
79
+ // The client revives errors without running a constructor, so this flag
80
+ // exists there only if the wire carries it. Without it the action runner
81
+ // wraps the error in a new ActionError without `handled`, POSTs it back to
82
+ // /api/client-error, and the server logs and reports it a second time.
83
+ isLowdefyError: true,
84
+ handled: err.handled
85
+ };
86
+ };
87
+ }
88
+ export default createWireProjection;
@@ -16,13 +16,12 @@
16
16
  import { type } from '@lowdefy/helpers';
17
17
  // extractErrorProps emits a nested error as a plain props object, not as a second
18
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.
19
+ // rather than on a map of where errors can appear means every position is visited
20
+ // without re-deriving where the walk puts errors.
22
21
  //
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.
22
+ // The check matters because the payload also carries author-written data - a
23
+ // UserError's non-Error cause, its metaData - and a `source` key inside those
24
+ // belongs to the app, not to us.
26
25
  function isErrorNode(value) {
27
26
  return type.isString(value.name) && type.isString(value.message);
28
27
  }
@@ -43,17 +42,15 @@ function stripConfigDirectory(value, prefix) {
43
42
  }
44
43
  Object.values(value).forEach((child)=>stripConfigDirectory(child, prefix));
45
44
  }
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.
45
+ // Makes `source` in the dev-only `devError` config-relative, never an absolute
46
+ // server path, so the dev tools show the path the developer knows. The wire error
47
+ // carries no `source` at all. resolveConfigLocation makes it absolute whenever the
48
+ // context carries a configDirectory, which server-dev does.
50
49
  //
51
50
  // A rewrite rather than an omission, and it needs the context, so it runs as a pass
52
51
  // over the serialized payload instead of inside the walk.
53
52
  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;
53
+ const configDirectory = context.configDirectory;
57
54
  if (type.isNone(configDirectory)) return payload;
58
55
  // configDirectory is `LOWDEFY_DIRECTORY_CONFIG || process.cwd()` at every site
59
56
  // that sets it, so it may be relative or carry a trailing separator, while
@@ -13,18 +13,52 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { serializer, type } from '@lowdefy/helpers';
16
+ import createWireProjection from './createWireProjection.js';
16
17
  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.
18
+ // The one error payload every route and transport sends for a thrown error, at
19
+ // any status. Owning the serialization as well as the policy leaves no bare
20
+ // serializer.serialize(error) in response position for a caller to forget.
21
+ //
22
+ // `~e` is the wire error - identical in dev and prod, because it is what app
23
+ // config reads. In dev the payload also carries `devError`, the error as the dev
24
+ // terminal logs it, for the dev tools. It sits beside `~e` rather than inside it:
25
+ // the serializer's reviver replaces any object holding `~e` with its error, so a
26
+ // plain deserialize of the payload drops `devError` and config never sees it.
27
+ //
28
+ // `received` is left out of devError at every node: on the request path it is
29
+ // the evaluated request properties, resolved _secret values included, and the
30
+ // dev server can be reached over a LAN or tunnel. The terminal still logs it.
31
+ function projectDevError(err) {
32
+ const props = {
33
+ message: err.message,
34
+ name: err.name,
35
+ stack: err.stack,
36
+ cause: err.cause,
37
+ ...err
38
+ };
39
+ delete props.received;
40
+ return props;
41
+ }
22
42
  function redactErrorResponse(context, error) {
23
43
  // Endpoint routes serialize the error field on success too, where it is null.
24
44
  // Passing that through unchanged keeps them from emitting an empty {'~e'}.
25
45
  if (type.isNone(error)) return error;
26
- return normalizeErrorSources(context, serializer.serialize(error, {
27
- omitErrorProps
28
- }));
46
+ const payload = serializer.serialize(error, {
47
+ projectError: createWireProjection(context)
48
+ });
49
+ // The server error handler has no request context when it fails before the
50
+ // context middleware runs, so there is no mode to read and no devError.
51
+ if (context?.mode === 'dev') {
52
+ const full = serializer.serialize(error, {
53
+ projectError: projectDevError
54
+ });
55
+ payload.devError = normalizeErrorSources(context, {
56
+ '~e': {
57
+ ...full['~e'],
58
+ requestId: context.rid
59
+ }
60
+ });
61
+ }
62
+ return payload;
29
63
  }
30
64
  export default redactErrorResponse;
@@ -13,19 +13,19 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { serializer } from '@lowdefy/helpers';
16
- import normalizeErrorSources from './normalizeErrorSources.js';
17
- import omitErrorProps from './omitErrorProps.js';
16
+ import createWireProjection from './createWireProjection.js';
18
17
  // The response-value call shape, beside redactErrorResponse's error-only one.
19
18
  // 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.
19
+ // in a value, so a response holding one is an error-serialization site too - a
20
+ // grep for serialize(error) cannot see those sites, which is why this exists as
21
+ // a function rather than as a rule to remember.
23
22
  //
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.
23
+ // Same wire projection as the error field, because it reaches the same audience.
24
+ // No devError, in any mode: an Error in a response value is data the routine
25
+ // returned, not a failure of this request.
26
26
  function redactResponse(context, response) {
27
- return normalizeErrorSources(context, serializer.serialize(response, {
28
- omitErrorProps
29
- }));
27
+ return serializer.serialize(response, {
28
+ projectError: createWireProjection(context)
29
+ });
30
30
  }
31
31
  export default redactResponse;
@@ -12,17 +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 { type } from '@lowdefy/helpers';
16
- import buildEndpointResult from '../../response/buildEndpointResult.js';
17
- import createEvaluateOperators from '../../context/createEvaluateOperators.js';
18
- import authorizeAgent from './authorizeAgent.js';
19
- import authorizeApiEndpoint from '../endpoints/authorizeApiEndpoint.js';
20
- import getEndpointConfig from '../endpoints/getEndpointConfig.js';
21
- import runRoutine from '../endpoints/runRoutine.js';
22
- import getAgentConfig from './getAgentConfig.js';
23
- import getAgentResolver from './getAgentResolver.js';
24
- import getConnectionConfig from '../connections/getConnectionConfig.js';
25
- import getConnection from '../connections/getConnection.js';
15
+ */ import createEvaluateOperators from '../../context/createEvaluateOperators.js';
16
+ import prepareAgent from './prepareAgent.js';
26
17
  async function callAgent(context, { agentId, pageId, messages, conversationId, urlQuery, sharedState }) {
27
18
  const { logger } = context;
28
19
  context.pageId = pageId;
@@ -32,12 +23,6 @@ async function callAgent(context, { agentId, pageId, messages, conversationId, u
32
23
  agentId,
33
24
  pageId
34
25
  });
35
- const agentConfig = await getAgentConfig(context, {
36
- agentId
37
- });
38
- authorizeAgent(context, {
39
- agentConfig
40
- });
41
26
  const agentContext = {
42
27
  conversationId: conversationId ?? undefined,
43
28
  pageId,
@@ -45,174 +30,12 @@ async function callAgent(context, { agentId, pageId, messages, conversationId, u
45
30
  urlQuery: urlQuery ?? {},
46
31
  userId: context.user?.sub ?? context.user?.id ?? null
47
32
  };
48
- // Evaluate operators in agent properties (e.g. _user, _secret, _payload)
49
- agentConfig.properties = context.evaluateOperators({
50
- input: agentConfig.properties ?? {},
51
- location: agentConfig.agentId,
52
- payload: agentContext,
53
- state: {},
54
- steps: {}
55
- });
56
- // Load connection config from build artifacts using agent's connectionId
57
- const connectionConfig = await getConnectionConfig(context, {
58
- connectionId: agentConfig.connectionId,
59
- configKey: agentConfig['~k']
60
- });
61
- // Get connection plugin from registry
62
- const connection = getConnection(context, {
63
- connectionConfig
64
- });
65
- // Evaluate operators in connection properties
66
- const connectionProperties = context.evaluateOperators({
67
- input: connectionConfig.properties || {},
68
- location: connectionConfig.connectionId,
69
- payload: {},
70
- state: {},
71
- steps: {}
72
- });
73
- // Create connection instance (e.g., Anthropic provider)
74
- const connectionInstance = connection.create({
75
- connection: connectionProperties
76
- });
77
- // Get agent type from plugin registry
78
- const agentType = getAgentResolver(context, {
79
- agentConfig
80
- });
81
- // Build resolver context with callEndpoint that allows InternalApi endpoints
82
- const resolverContext = {
33
+ const { agentConfig, connectionInstance, agentType, resolverContext } = await prepareAgent(context, {
34
+ agentId,
83
35
  agentContext,
84
- i18n: context.i18n,
85
- evaluateOperators: (input)=>context.evaluateOperators({
86
- input,
87
- location: agentConfig.agentId,
88
- payload: agentContext,
89
- state: {},
90
- steps: {}
91
- }),
92
- callEndpoint: async (endpointId, { payload, abortSignal })=>{
93
- const endpointConfig = await getEndpointConfig(context, {
94
- endpointId
95
- });
96
- authorizeApiEndpoint(context, {
97
- endpointConfig
98
- });
99
- const routineContext = {
100
- steps: {},
101
- payload: payload ?? {},
102
- arrayIndices: [],
103
- items: {},
104
- state: {},
105
- endpointDepth: 0
106
- };
107
- const { error, response, status } = await runRoutine(context, routineContext, {
108
- routine: endpointConfig.routine
109
- });
110
- return buildEndpointResult(context, {
111
- error,
112
- response,
113
- status
114
- });
115
- },
116
- getEndpointConfig: async ({ endpointId })=>{
117
- return getEndpointConfig(context, {
118
- endpointId
119
- });
120
- },
121
- getAgentConfig: async ({ agentId })=>{
122
- const subAgentConfig = await getAgentConfig(context, {
123
- agentId
124
- });
125
- authorizeAgent(context, {
126
- agentConfig: subAgentConfig
127
- });
128
- return subAgentConfig;
129
- },
130
- getConnectionForAgent: async ({ agentConfig: subAgentConfig })=>{
131
- const subConnectionConfig = await getConnectionConfig(context, {
132
- connectionId: subAgentConfig.connectionId,
133
- configKey: subAgentConfig['~k']
134
- });
135
- const subConnection = getConnection(context, {
136
- connectionConfig: subConnectionConfig
137
- });
138
- const subConnectionProperties = context.evaluateOperators({
139
- input: subConnectionConfig.properties || {},
140
- location: subConnectionConfig.connectionId,
141
- payload: {},
142
- state: {},
143
- steps: {}
144
- });
145
- return subConnection.create({
146
- connection: subConnectionProperties
147
- });
148
- },
149
- resolveMcpSources: async ({ agentConfig: subAgentConfig })=>{
150
- const resolvedMcp = [];
151
- for (const mcpSource of subAgentConfig.mcp ?? []){
152
- if (!type.isNone(mcpSource.connectionId)) {
153
- const mcpConnConfig = await getConnectionConfig(context, {
154
- connectionId: mcpSource.connectionId,
155
- configKey: subAgentConfig['~k']
156
- });
157
- const mcpConnection = getConnection(context, {
158
- connectionConfig: mcpConnConfig
159
- });
160
- const mcpConnProps = context.evaluateOperators({
161
- input: mcpConnConfig.properties || {},
162
- location: mcpConnConfig.connectionId,
163
- payload: {},
164
- state: {},
165
- steps: {}
166
- });
167
- const mcpConfig = mcpConnection.create({
168
- connection: mcpConnProps
169
- });
170
- const { connectionId: _, ...overrides } = mcpSource;
171
- resolvedMcp.push({
172
- ...mcpConfig,
173
- ...overrides
174
- });
175
- } else {
176
- resolvedMcp.push(mcpSource);
177
- }
178
- }
179
- return resolvedMcp;
180
- }
181
- };
182
- // Resolve MCP connection references to inline config.
183
- // Agent-level overrides (like confirm) may still contain operators —
184
- // handleAgentChat evaluates those via its existing evaluateOperators call.
185
- const resolvedMcp = [];
186
- for (const mcpSource of agentConfig.mcp ?? []){
187
- if (!type.isNone(mcpSource.connectionId)) {
188
- const mcpConnConfig = await getConnectionConfig(context, {
189
- connectionId: mcpSource.connectionId,
190
- configKey: agentConfig['~k']
191
- });
192
- const mcpConnection = getConnection(context, {
193
- connectionConfig: mcpConnConfig
194
- });
195
- const mcpConnProps = context.evaluateOperators({
196
- input: mcpConnConfig.properties || {},
197
- location: mcpConnConfig.connectionId,
198
- payload: {},
199
- state: {},
200
- steps: {}
201
- });
202
- const mcpConfig = mcpConnection.create({
203
- connection: mcpConnProps
204
- });
205
- // Merge: connection properties as base, agent-level overrides on top
206
- const { connectionId: _, ...overrides } = mcpSource;
207
- resolvedMcp.push({
208
- ...mcpConfig,
209
- ...overrides
210
- });
211
- } else {
212
- resolvedMcp.push(mcpSource);
213
- }
214
- }
215
- agentConfig.mcp = resolvedMcp;
36
+ endpointDepth: 0,
37
+ mode: 'chat'
38
+ });
216
39
  // Call the agent resolver
217
40
  const { response } = await agentType.resolver({
218
41
  connection: connectionInstance,