@lowdefy/api 5.5.1 → 6.0.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 (56) hide show
  1. package/dist/context/createAuthorize.js +4 -1
  2. package/dist/context/resolveStrategyCaller.js +56 -0
  3. package/dist/index.js +16 -3
  4. package/dist/response/buildEndpointResult.js +33 -0
  5. package/dist/response/normalizeErrorSources.js +64 -0
  6. package/dist/response/omitErrorProps.js +51 -0
  7. package/dist/response/redactErrorResponse.js +30 -0
  8. package/dist/response/redactResponse.js +31 -0
  9. package/dist/routes/agent/authorizeAgent.js +39 -0
  10. package/dist/routes/agent/callAgent.js +7 -180
  11. package/dist/routes/agent/prepareAgent.js +173 -0
  12. package/dist/routes/auth/createLogger.js +2 -5
  13. package/dist/routes/auth/createPrefixedCookies.js +52 -0
  14. package/dist/routes/auth/{getNextAuthConfig.js → getAuthConfig.js} +27 -16
  15. package/dist/routes/auth/resolveCookies.js +37 -0
  16. package/dist/routes/auth/strategies/createAuthStrategies.js +77 -0
  17. package/dist/routes/auth/strategies/getAuthStrategies.js +31 -0
  18. package/dist/routes/endpoints/addStepResult.js +12 -2
  19. package/dist/routes/endpoints/authorizeApiEndpoint.js +8 -2
  20. package/dist/routes/endpoints/callEndpoint.js +32 -13
  21. package/dist/routes/endpoints/control/controlReject.js +3 -1
  22. package/dist/routes/endpoints/control/controlSetState.js +13 -2
  23. package/dist/routes/endpoints/control/controlThrow.js +4 -1
  24. package/dist/routes/endpoints/findSchedule.js +35 -0
  25. package/dist/routes/endpoints/forwardScheduledEndpoint.js +106 -0
  26. package/dist/routes/endpoints/getEndpointConfig.js +9 -3
  27. package/dist/routes/endpoints/getEnvironmentSchedules.js +29 -0
  28. package/dist/routes/endpoints/handleAgentCall.js +83 -0
  29. package/dist/routes/endpoints/handleEndpointCall.js +49 -1
  30. package/dist/routes/endpoints/handleRenderNotification.js +189 -0
  31. package/dist/routes/endpoints/handleValidateSchema.js +3 -1
  32. package/dist/routes/endpoints/isUnauthenticatedHuman.js +31 -0
  33. package/dist/routes/endpoints/resolveCronEnvironment.js +37 -0
  34. package/dist/routes/endpoints/runDetachedEndpoint.js +66 -0
  35. package/dist/routes/endpoints/runRoutine.js +15 -1
  36. package/dist/routes/endpoints/runScheduledEndpoint.js +104 -0
  37. package/dist/routes/endpoints/runWebhookEndpoint.js +83 -0
  38. package/dist/routes/endpoints/scheduleBackground.js +48 -0
  39. package/dist/routes/mcp/createMcpServer.js +160 -0
  40. package/dist/routes/notifications/derivePreview.js +30 -0
  41. package/dist/routes/notifications/getNotificationConfig.js +32 -0
  42. package/dist/routes/notifications/resolveNotificationLinks.js +72 -0
  43. package/dist/routes/notifications/resolveThemeLogo.js +35 -0
  44. package/dist/routes/page/dynamic/resolveDynamicContent.js +172 -0
  45. package/dist/routes/page/dynamic/unescapeOperators.js +36 -0
  46. package/dist/routes/page/dynamic/validateFragment.js +104 -0
  47. package/dist/routes/page/getPageConfig.js +16 -6
  48. package/dist/routes/request/callRequest.js +2 -1
  49. package/dist/routes/websocket/authorizeWebsocket.js +27 -0
  50. package/dist/routes/websocket/createChannelRegistry.js +269 -0
  51. package/dist/routes/websocket/createWebSocketConnection.js +131 -0
  52. package/dist/routes/websocket/getWebsocketConfig.js +30 -0
  53. package/dist/routes/websocket/getWebsocketResolver.js +33 -0
  54. package/dist/routes/websocket/prepareChannel.js +72 -0
  55. package/dist/test/testContext.js +10 -3
  56. package/package.json +12 -10
@@ -0,0 +1,173 @@
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
+ import buildEndpointResult from '../../response/buildEndpointResult.js';
17
+ import getEndpointConfig from '../endpoints/getEndpointConfig.js';
18
+ import invokeEndpoint from '../endpoints/invokeEndpoint.js';
19
+ import authorizeAgent from './authorizeAgent.js';
20
+ import getAgentConfig from './getAgentConfig.js';
21
+ import getAgentResolver from './getAgentResolver.js';
22
+ import getConnectionConfig from '../connections/getConnectionConfig.js';
23
+ import getConnection from '../connections/getConnection.js';
24
+ // Shared agent invocation construction for the streaming chat route (callAgent)
25
+ // and the headless CallAgent routine step (handleAgentCall): loads the agent
26
+ // config, evaluates operators, creates the provider connection, and builds the
27
+ // resolver context. mode ('chat' | 'generate') selects the resolver's execution
28
+ // path; endpointDepth threads the endpoint call depth cap through agent tool
29
+ // and hook endpoint calls.
30
+ async function prepareAgent(context, { agentId, agentContext, endpointDepth = 0, mode = 'chat' }) {
31
+ const agentConfig = await getAgentConfig(context, {
32
+ agentId
33
+ });
34
+ authorizeAgent(context, {
35
+ agentConfig
36
+ });
37
+ // Evaluate operators in agent properties (e.g. _user, _secret, _payload)
38
+ agentConfig.properties = context.evaluateOperators({
39
+ input: agentConfig.properties ?? {},
40
+ location: agentConfig.agentId,
41
+ payload: agentContext,
42
+ state: {},
43
+ steps: {}
44
+ });
45
+ // Load connection config from build artifacts using agent's connectionId
46
+ const connectionConfig = await getConnectionConfig(context, {
47
+ connectionId: agentConfig.connectionId,
48
+ configKey: agentConfig['~k']
49
+ });
50
+ // Get connection plugin from registry
51
+ const connection = getConnection(context, {
52
+ connectionConfig
53
+ });
54
+ // Evaluate operators in connection properties
55
+ const connectionProperties = context.evaluateOperators({
56
+ input: connectionConfig.properties || {},
57
+ location: connectionConfig.connectionId,
58
+ payload: {},
59
+ state: {},
60
+ steps: {}
61
+ });
62
+ // Create connection instance (e.g., Anthropic provider)
63
+ const connectionInstance = connection.create({
64
+ connection: connectionProperties
65
+ });
66
+ // Get agent type from plugin registry
67
+ const agentType = getAgentResolver(context, {
68
+ agentConfig
69
+ });
70
+ // Build resolver context with callEndpoint that allows InternalApi endpoints
71
+ const resolverContext = {
72
+ agentContext,
73
+ i18n: context.i18n,
74
+ mode,
75
+ evaluateOperators: (input)=>context.evaluateOperators({
76
+ input,
77
+ location: agentConfig.agentId,
78
+ payload: agentContext,
79
+ state: {},
80
+ steps: {}
81
+ }),
82
+ callEndpoint: async (endpointId, { payload })=>{
83
+ const { error, response, status } = await invokeEndpoint(context, {
84
+ endpointId,
85
+ payload,
86
+ endpointDepth
87
+ });
88
+ return buildEndpointResult(context, {
89
+ error,
90
+ response,
91
+ status
92
+ });
93
+ },
94
+ getEndpointConfig: async ({ endpointId })=>{
95
+ return getEndpointConfig(context, {
96
+ endpointId
97
+ });
98
+ },
99
+ getAgentConfig: async ({ agentId: subAgentId })=>{
100
+ const subAgentConfig = await getAgentConfig(context, {
101
+ agentId: subAgentId
102
+ });
103
+ authorizeAgent(context, {
104
+ agentConfig: subAgentConfig
105
+ });
106
+ return subAgentConfig;
107
+ },
108
+ getConnectionForAgent: async ({ agentConfig: subAgentConfig })=>{
109
+ const subConnectionConfig = await getConnectionConfig(context, {
110
+ connectionId: subAgentConfig.connectionId,
111
+ configKey: subAgentConfig['~k']
112
+ });
113
+ const subConnection = getConnection(context, {
114
+ connectionConfig: subConnectionConfig
115
+ });
116
+ const subConnectionProperties = context.evaluateOperators({
117
+ input: subConnectionConfig.properties || {},
118
+ location: subConnectionConfig.connectionId,
119
+ payload: {},
120
+ state: {},
121
+ steps: {}
122
+ });
123
+ return subConnection.create({
124
+ connection: subConnectionProperties
125
+ });
126
+ },
127
+ resolveMcpSources: async ({ agentConfig: subAgentConfig })=>{
128
+ const resolvedMcp = [];
129
+ for (const mcpSource of subAgentConfig.mcp ?? []){
130
+ if (!type.isNone(mcpSource.connectionId)) {
131
+ const mcpConnConfig = await getConnectionConfig(context, {
132
+ connectionId: mcpSource.connectionId,
133
+ configKey: subAgentConfig['~k']
134
+ });
135
+ const mcpConnection = getConnection(context, {
136
+ connectionConfig: mcpConnConfig
137
+ });
138
+ const mcpConnProps = context.evaluateOperators({
139
+ input: mcpConnConfig.properties || {},
140
+ location: mcpConnConfig.connectionId,
141
+ payload: {},
142
+ state: {},
143
+ steps: {}
144
+ });
145
+ const mcpConfig = mcpConnection.create({
146
+ connection: mcpConnProps
147
+ });
148
+ const { connectionId: _, ...overrides } = mcpSource;
149
+ resolvedMcp.push({
150
+ ...mcpConfig,
151
+ ...overrides
152
+ });
153
+ } else {
154
+ resolvedMcp.push(mcpSource);
155
+ }
156
+ }
157
+ return resolvedMcp;
158
+ }
159
+ };
160
+ // Resolve MCP connection references to inline config.
161
+ // Agent-level overrides (like confirm) may still contain operators —
162
+ // handleAgentChat evaluates those via its existing evaluateOperators call.
163
+ agentConfig.mcp = await resolverContext.resolveMcpSources({
164
+ agentConfig
165
+ });
166
+ return {
167
+ agentConfig,
168
+ connectionInstance,
169
+ agentType,
170
+ resolverContext
171
+ };
172
+ }
173
+ export default prepareAgent;
@@ -20,11 +20,8 @@
20
20
  };
21
21
  function createLogger({ logger }) {
22
22
  return {
23
- error: (code, metadata)=>{
24
- const error = metadata instanceof Error ? metadata : metadata?.error;
25
- if (error) {
26
- error.code = code;
27
- }
23
+ // Auth.js v5 logger contract: error(error), warn(code), debug(message, metadata).
24
+ error: (error)=>{
28
25
  logger.error(error);
29
26
  },
30
27
  warn: (code)=>{
@@ -0,0 +1,52 @@
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
+ */ function slugifyPrefix(value) {
16
+ return String(value).trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
17
+ }
18
+ // Injects an app-specific prefix into each Auth.js cookie name so multiple apps
19
+ // on the same host do not share a cookie jar (browsers do not scope cookies by
20
+ // port). Only names are overridden — Auth.js v5 deep-merges `config.cookies`
21
+ // into its defaults, so cookie options (httpOnly, secure, maxAge) stay in sync
22
+ // with @auth/core's per-request defaults. Names mirror defaultCookies in
23
+ // @auth/core/lib/utils/cookie.js.
24
+ function createPrefixedCookies({ prefix, useSecureCookies }) {
25
+ const securePrefix = useSecureCookies ? '__Secure-' : '';
26
+ const hostPrefix = useSecureCookies ? '__Host-' : '';
27
+ return {
28
+ sessionToken: {
29
+ name: `${securePrefix}${prefix}.authjs.session-token`
30
+ },
31
+ callbackUrl: {
32
+ name: `${securePrefix}${prefix}.authjs.callback-url`
33
+ },
34
+ csrfToken: {
35
+ name: `${hostPrefix}${prefix}.authjs.csrf-token`
36
+ },
37
+ pkceCodeVerifier: {
38
+ name: `${securePrefix}${prefix}.authjs.pkce.code_verifier`
39
+ },
40
+ state: {
41
+ name: `${securePrefix}${prefix}.authjs.state`
42
+ },
43
+ nonce: {
44
+ name: `${securePrefix}${prefix}.authjs.nonce`
45
+ },
46
+ webauthnChallenge: {
47
+ name: `${securePrefix}${prefix}.authjs.challenge`
48
+ }
49
+ };
50
+ }
51
+ export { slugifyPrefix };
52
+ export default createPrefixedCookies;
@@ -19,10 +19,11 @@ import createCallbacks from './callbacks/createCallbacks.js';
19
19
  import createEvents from './events/createEvents.js';
20
20
  import createLogger from './createLogger.js';
21
21
  import createProviders from './createProviders.js';
22
- const nextAuthConfig = {};
22
+ import resolveCookies from './resolveCookies.js';
23
+ const authConfigCache = {};
23
24
  let initialized = false;
24
- function getNextAuthConfig({ appMeta, authJson, logger, plugins, secrets }) {
25
- if (initialized) return nextAuthConfig;
25
+ function getAuthConfig({ appMeta, authJson, dev, logger, plugins, secrets }) {
26
+ if (initialized) return authConfigCache;
26
27
  const operatorsParser = new ServerParser({
27
28
  lowdefyApp: appMeta,
28
29
  operators: {
@@ -40,36 +41,46 @@ function getNextAuthConfig({ appMeta, authJson, logger, plugins, secrets }) {
40
41
  if (operatorErrors.length > 0) {
41
42
  throw operatorErrors[0];
42
43
  }
43
- nextAuthConfig.adapter = createAdapter({
44
+ authConfigCache.adapter = createAdapter({
44
45
  authConfig,
45
46
  logger,
46
47
  plugins
47
48
  });
48
- nextAuthConfig.callbacks = createCallbacks({
49
+ authConfigCache.callbacks = createCallbacks({
49
50
  authConfig,
50
51
  logger,
51
52
  plugins
52
53
  });
53
- nextAuthConfig.events = createEvents({
54
+ authConfigCache.events = createEvents({
54
55
  authConfig,
55
56
  logger,
56
57
  plugins
57
58
  });
58
- nextAuthConfig.logger = createLogger({
59
+ authConfigCache.logger = createLogger({
59
60
  logger
60
61
  });
61
- nextAuthConfig.providers = createProviders({
62
+ authConfigCache.providers = createProviders({
62
63
  authConfig,
63
64
  logger,
64
65
  plugins
65
66
  });
66
- nextAuthConfig.debug = authConfig.debug ?? logger?.isLevelEnabled('debug') === true;
67
- nextAuthConfig.pages = authConfig.authPages;
68
- nextAuthConfig.session = authConfig.session;
69
- nextAuthConfig.theme = authConfig.theme;
70
- nextAuthConfig.cookies = authConfig?.advanced?.cookies;
71
- nextAuthConfig.originalRedirectCallback = nextAuthConfig.callbacks.redirect;
67
+ authConfigCache.debug = authConfig.debug ?? logger?.isLevelEnabled('debug') === true;
68
+ authConfigCache.pages = authConfig.authPages;
69
+ authConfigCache.session = authConfig.session;
70
+ authConfigCache.theme = authConfig.theme;
71
+ authConfigCache.cookies = resolveCookies({
72
+ appMeta,
73
+ authConfig,
74
+ dev
75
+ });
76
+ // Auth.js v5 reads AUTH_SECRET; the v4 NEXTAUTH_SECRET variable is not
77
+ // supported — deployments must rename it (see the v5-to-v6 migration doc).
78
+ authConfigCache.secret = process.env.AUTH_SECRET;
79
+ // Self-hosted servers run behind arbitrary proxies; derive URLs from request
80
+ // headers (v4 derived them from NEXTAUTH_URL, aliased to AUTH_URL at startup).
81
+ authConfigCache.trustHost = true;
82
+ authConfigCache.basePath = '/api/auth';
72
83
  initialized = true;
73
- return nextAuthConfig;
84
+ return authConfigCache;
74
85
  }
75
- export default getNextAuthConfig;
86
+ export default getAuthConfig;
@@ -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
+ */ import { type } from '@lowdefy/helpers';
16
+ import createPrefixedCookies, { slugifyPrefix } from './createPrefixedCookies.js';
17
+ // Cookie name resolution, in order of precedence:
18
+ // 1. An explicit `auth.advanced.cookies` object is used verbatim.
19
+ // 2. An explicit `auth.advanced.cookiePrefix` namespaces cookie names (dev and prod).
20
+ // 3. When running the dev server, derive a prefix from the app slug/name so multiple
21
+ // apps on localhost do not share a cookie jar (browsers do not scope cookies by
22
+ // port). Production is left untouched, returning undefined for Auth.js defaults.
23
+ function resolveCookies({ appMeta, authConfig, dev }) {
24
+ const explicitCookies = authConfig?.advanced?.cookies;
25
+ if (!type.isNone(explicitCookies)) return explicitCookies;
26
+ const devPrefix = dev ? slugifyPrefix(appMeta?.slug ?? appMeta?.name ?? '') : '';
27
+ const prefix = authConfig?.advanced?.cookiePrefix ?? (devPrefix === '' ? undefined : devPrefix);
28
+ if (type.isNone(prefix) || prefix === '') return undefined;
29
+ // Auth.js v5 reads AUTH_URL with NEXTAUTH_URL as a v4 fallback.
30
+ const authUrl = process.env.AUTH_URL ?? process.env.NEXTAUTH_URL;
31
+ const useSecureCookies = authUrl?.startsWith('https://') ?? false;
32
+ return createPrefixedCookies({
33
+ prefix,
34
+ useSecureCookies
35
+ });
36
+ }
37
+ export default resolveCookies;
@@ -0,0 +1,77 @@
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 { ServerParser } from '@lowdefy/operators';
16
+ import { _app, _secret } from '@lowdefy/operators-js/operators/server';
17
+ import { type } from '@lowdefy/helpers';
18
+ import { ConfigError } from '@lowdefy/errors';
19
+ // Builds the API auth strategy verifiers from the auth.json build artifact.
20
+ // Build has validated the config and written all defaults, so this resolves
21
+ // the _secret operators and constructs each strategy type's verifier.
22
+ // Verifier factories warn on short static keys at construction, so key
23
+ // strength surfaces at startup, never as a build failure.
24
+ function createAuthStrategies({ appMeta, authJson, logger, plugins, secrets }) {
25
+ if (!type.isArray(authJson.strategies) || authJson.strategies.length === 0) {
26
+ return [];
27
+ }
28
+ const operatorsParser = new ServerParser({
29
+ lowdefyApp: appMeta,
30
+ operators: {
31
+ _app,
32
+ _secret
33
+ },
34
+ secrets,
35
+ user: {}
36
+ });
37
+ const { output: strategiesConfig, errors: operatorErrors } = operatorsParser.parse({
38
+ input: authJson.strategies,
39
+ location: 'auth.strategies',
40
+ payload: {}
41
+ });
42
+ if (operatorErrors.length > 0) {
43
+ // Startup fails on the first error; log the rest so they can all be
44
+ // fixed in one pass instead of one boot per error.
45
+ operatorErrors.slice(1).forEach((error)=>logger.error(error));
46
+ throw operatorErrors[0];
47
+ }
48
+ return strategiesConfig.map((strategy)=>{
49
+ const strategyPlugin = plugins.strategies[strategy.type];
50
+ if (type.isNone(strategyPlugin)) {
51
+ throw new ConfigError(`Auth strategy type "${strategy.type}" not found at strategy "${strategy.id}".`, {
52
+ configKey: strategy['~k']
53
+ });
54
+ }
55
+ let verify;
56
+ try {
57
+ verify = strategyPlugin({
58
+ logger,
59
+ properties: strategy.properties,
60
+ strategyId: strategy.id
61
+ });
62
+ } catch (error) {
63
+ throw new ConfigError(error.message, {
64
+ cause: error,
65
+ configKey: strategy['~k']
66
+ });
67
+ }
68
+ return {
69
+ attributes: strategy.attributes,
70
+ id: strategy.id,
71
+ roles: strategy.roles,
72
+ type: strategy.type,
73
+ verify
74
+ };
75
+ });
76
+ }
77
+ export default createAuthStrategies;
@@ -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 createAuthStrategies from './createAuthStrategies.js';
16
+ let strategies;
17
+ // The strategy verifiers are constructed once per process at first use -
18
+ // the server middleware tries them in config order on every request that
19
+ // resolves no session.
20
+ function getAuthStrategies({ appMeta, authJson, logger, plugins, secrets }) {
21
+ if (strategies) return strategies;
22
+ strategies = createAuthStrategies({
23
+ appMeta,
24
+ authJson,
25
+ logger,
26
+ plugins,
27
+ secrets
28
+ });
29
+ return strategies;
30
+ }
31
+ export default getAuthStrategies;
@@ -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;
@@ -12,14 +12,20 @@
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 { ConfigError } from '@lowdefy/errors';
16
- function authorizeApiEndpoint({ authorize, logger }, { endpointConfig }) {
15
+ */ import { AuthenticationError, ConfigError } from '@lowdefy/errors';
16
+ import { type } from '@lowdefy/helpers';
17
+ function authorizeApiEndpoint({ authorize, logger, user }, { endpointConfig }) {
17
18
  if (!authorize(endpointConfig)) {
18
19
  logger.debug({
19
20
  event: 'debug_api_authorize',
20
21
  authorized: false,
21
22
  auth_config: endpointConfig.auth
22
23
  });
24
+ // Unauthenticated on a protected endpoint - 401 tells the caller to fix
25
+ // its credentials. Wrong roles stay opaque below.
26
+ if (type.isNone(user)) {
27
+ throw new AuthenticationError(`Authentication required for API endpoint "${endpointConfig.endpointId}".`);
28
+ }
23
29
  throw new ConfigError(`API Endpoint "${endpointConfig.endpointId}" does not exist.`);
24
30
  }
25
31
  logger.debug({
@@ -13,11 +13,14 @@
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 { ConfigError } from '@lowdefy/errors';
16
+ import { AuthenticationError, 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';
21
+ import isUnauthenticatedHuman from './isUnauthenticatedHuman.js';
20
22
  import runRoutine from './runRoutine.js';
23
+ import scheduleBackground from './scheduleBackground.js';
21
24
  async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
22
25
  const { logger } = context;
23
26
  context.blockId = blockId;
@@ -34,9 +37,11 @@ async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
34
37
  const endpointConfig = await getEndpointConfig(context, {
35
38
  endpointId
36
39
  });
37
- // Block HTTP access to InternalApi endpoints same error as missing endpoint
40
+ // Block HTTP access to InternalApi endpoints - same error as a missing
41
+ // endpoint, including the unauthenticated fork, so an internal endpoint is
42
+ // indistinguishable from one that does not exist on both paths.
38
43
  if (endpointConfig.type === 'InternalApi') {
39
- const err = new ConfigError(`API Endpoint "${endpointId}" does not exist.`);
44
+ const err = await isUnauthenticatedHuman(context) ? new AuthenticationError(`Authentication required for API endpoint "${endpointId}".`) : new ConfigError(`API Endpoint "${endpointId}" does not exist.`);
40
45
  logger.debug({
41
46
  params: {
42
47
  endpointId
@@ -56,18 +61,32 @@ async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
56
61
  state: {},
57
62
  endpointDepth: 0
58
63
  };
64
+ // async: true — acknowledge now, run the routine in the background.
65
+ // Auth was already checked above; the outcome lands in logs (scheduleBackground)
66
+ // and in whatever the routine itself records.
67
+ if (endpointConfig.async === true) {
68
+ scheduleBackground(context, {
69
+ event: 'background_endpoint',
70
+ endpointId
71
+ }, ()=>runRoutine(context, routineContext, {
72
+ routine: endpointConfig.routine
73
+ }));
74
+ return {
75
+ error: null,
76
+ response: serializer.serialize({
77
+ accepted: true
78
+ }),
79
+ status: 'accepted',
80
+ success: true
81
+ };
82
+ }
59
83
  const { error, response, status } = await runRoutine(context, routineContext, {
60
84
  routine: endpointConfig.routine
61
85
  });
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
- };
86
+ return buildEndpointResult(context, {
87
+ error,
88
+ response,
89
+ status
90
+ });
72
91
  }
73
92
  export default callEndpoint;
@@ -37,9 +37,11 @@ async function controlReject(context, routineContext, { control }) {
37
37
  cause,
38
38
  isReject: true
39
39
  });
40
+ // Log under `err` — see controlThrow: only the `err` key runs the pino error
41
+ // serializer, so `error` would drop the message from the log line.
40
42
  context.logger.warn({
41
43
  event: 'warn_control_reject',
42
- error
44
+ err: error
43
45
  });
44
46
  return {
45
47
  status: 'reject',
@@ -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'
@@ -36,9 +36,12 @@ async function controlThrow(context, routineContext, { control }) {
36
36
  const error = new UserError(message, {
37
37
  cause
38
38
  });
39
+ // Log under `err` — the pino error serializer (createNodeLogger) is registered
40
+ // for the `err` key only; an Error passed as `error` is JSON-dumped without its
41
+ // non-enumerable `message`/`stack`, producing a log line with no message.
39
42
  context.logger.error({
40
43
  event: 'error_control_throw',
41
- error
44
+ err: error
42
45
  });
43
46
  return {
44
47
  status: 'error',