@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
@@ -0,0 +1,179 @@
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 createWireProjection from '../../response/createWireProjection.js';
18
+ import getEndpointConfig from '../endpoints/getEndpointConfig.js';
19
+ import invokeEndpoint from '../endpoints/invokeEndpoint.js';
20
+ import authorizeAgent from './authorizeAgent.js';
21
+ import getAgentConfig from './getAgentConfig.js';
22
+ import getAgentResolver from './getAgentResolver.js';
23
+ import getConnectionConfig from '../connections/getConnectionConfig.js';
24
+ import getConnection from '../connections/getConnection.js';
25
+ // Shared agent invocation construction for the streaming chat route (callAgent)
26
+ // and the headless CallAgent routine step (handleAgentCall): loads the agent
27
+ // config, evaluates operators, creates the provider connection, and builds the
28
+ // resolver context. mode ('chat' | 'generate') selects the resolver's execution
29
+ // path; endpointDepth threads the endpoint call depth cap through agent tool
30
+ // and hook endpoint calls.
31
+ async function prepareAgent(context, { agentId, agentContext, endpointDepth = 0, mode = 'chat' }) {
32
+ const agentConfig = await getAgentConfig(context, {
33
+ agentId
34
+ });
35
+ authorizeAgent(context, {
36
+ agentConfig
37
+ });
38
+ // Evaluate operators in agent properties (e.g. _user, _secret, _payload)
39
+ agentConfig.properties = context.evaluateOperators({
40
+ input: agentConfig.properties ?? {},
41
+ location: agentConfig.agentId,
42
+ payload: agentContext,
43
+ state: {},
44
+ steps: {}
45
+ });
46
+ // Load connection config from build artifacts using agent's connectionId
47
+ const connectionConfig = await getConnectionConfig(context, {
48
+ connectionId: agentConfig.connectionId,
49
+ configKey: agentConfig['~k']
50
+ });
51
+ // Get connection plugin from registry
52
+ const connection = getConnection(context, {
53
+ connectionConfig
54
+ });
55
+ // Evaluate operators in connection properties
56
+ const connectionProperties = context.evaluateOperators({
57
+ input: connectionConfig.properties || {},
58
+ location: connectionConfig.connectionId,
59
+ payload: {},
60
+ state: {},
61
+ steps: {}
62
+ });
63
+ // Create connection instance (e.g., Anthropic provider)
64
+ const connectionInstance = connection.create({
65
+ connection: connectionProperties
66
+ });
67
+ // Get agent type from plugin registry
68
+ const agentType = getAgentResolver(context, {
69
+ agentConfig
70
+ });
71
+ // Build resolver context with callEndpoint that allows InternalApi endpoints
72
+ const resolverContext = {
73
+ agentContext,
74
+ i18n: context.i18n,
75
+ mode,
76
+ // The agent stream's error text reaches the end user and AgentChat config, so it takes
77
+ // the wire policy. Built here because the agent plugins cannot import @lowdefy/api.
78
+ // The AI SDK types onError's argument as unknown: a tool can reject with null or a
79
+ // string, which takes the generic message like any other non-author error.
80
+ wireErrorMessage: (error)=>createWireProjection(context)(type.isError(error) ? error : new Error(String(error))).message,
81
+ evaluateOperators: (input)=>context.evaluateOperators({
82
+ input,
83
+ location: agentConfig.agentId,
84
+ payload: agentContext,
85
+ state: {},
86
+ steps: {}
87
+ }),
88
+ callEndpoint: async (endpointId, { payload })=>{
89
+ const { error, response, status } = await invokeEndpoint(context, {
90
+ endpointId,
91
+ payload,
92
+ endpointDepth
93
+ });
94
+ return buildEndpointResult(context, {
95
+ error,
96
+ response,
97
+ status
98
+ });
99
+ },
100
+ getEndpointConfig: async ({ endpointId })=>{
101
+ return getEndpointConfig(context, {
102
+ endpointId
103
+ });
104
+ },
105
+ getAgentConfig: async ({ agentId: subAgentId })=>{
106
+ const subAgentConfig = await getAgentConfig(context, {
107
+ agentId: subAgentId
108
+ });
109
+ authorizeAgent(context, {
110
+ agentConfig: subAgentConfig
111
+ });
112
+ return subAgentConfig;
113
+ },
114
+ getConnectionForAgent: async ({ agentConfig: subAgentConfig })=>{
115
+ const subConnectionConfig = await getConnectionConfig(context, {
116
+ connectionId: subAgentConfig.connectionId,
117
+ configKey: subAgentConfig['~k']
118
+ });
119
+ const subConnection = getConnection(context, {
120
+ connectionConfig: subConnectionConfig
121
+ });
122
+ const subConnectionProperties = context.evaluateOperators({
123
+ input: subConnectionConfig.properties || {},
124
+ location: subConnectionConfig.connectionId,
125
+ payload: {},
126
+ state: {},
127
+ steps: {}
128
+ });
129
+ return subConnection.create({
130
+ connection: subConnectionProperties
131
+ });
132
+ },
133
+ resolveMcpSources: async ({ agentConfig: subAgentConfig })=>{
134
+ const resolvedMcp = [];
135
+ for (const mcpSource of subAgentConfig.mcp ?? []){
136
+ if (!type.isNone(mcpSource.connectionId)) {
137
+ const mcpConnConfig = await getConnectionConfig(context, {
138
+ connectionId: mcpSource.connectionId,
139
+ configKey: subAgentConfig['~k']
140
+ });
141
+ const mcpConnection = getConnection(context, {
142
+ connectionConfig: mcpConnConfig
143
+ });
144
+ const mcpConnProps = context.evaluateOperators({
145
+ input: mcpConnConfig.properties || {},
146
+ location: mcpConnConfig.connectionId,
147
+ payload: {},
148
+ state: {},
149
+ steps: {}
150
+ });
151
+ const mcpConfig = mcpConnection.create({
152
+ connection: mcpConnProps
153
+ });
154
+ const { connectionId: _, ...overrides } = mcpSource;
155
+ resolvedMcp.push({
156
+ ...mcpConfig,
157
+ ...overrides
158
+ });
159
+ } else {
160
+ resolvedMcp.push(mcpSource);
161
+ }
162
+ }
163
+ return resolvedMcp;
164
+ }
165
+ };
166
+ // Resolve MCP connection references to inline config.
167
+ // Agent-level overrides (like confirm) may still contain operators —
168
+ // handleAgentChat evaluates those via its existing evaluateOperators call.
169
+ agentConfig.mcp = await resolverContext.resolveMcpSources({
170
+ agentConfig
171
+ });
172
+ return {
173
+ agentConfig,
174
+ connectionInstance,
175
+ agentType,
176
+ resolverContext
177
+ };
178
+ }
179
+ 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;
@@ -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 { type } from '@lowdefy/helpers';
16
+ import getCurrentEnvironment from '../../context/getCurrentEnvironment.js';
17
+ // Auth.js reads its base URL from AUTH_URL (NEXTAUTH_URL is aliased to it at startup). Default it to
18
+ // the current environment's url so a deployment declared in config.environments needs no separate
19
+ // AUTH_URL / NEXTAUTH_URL. The dev server build drops the current environment's url, so the dev
20
+ // server keeps the request origin.
21
+ function defaultAuthUrl({ config }) {
22
+ // Same "is it set" test as resolveCookies, so both agree on whether an auth URL is configured.
23
+ if (!type.isNone(process.env.AUTH_URL ?? process.env.NEXTAUTH_URL)) return;
24
+ const url = getCurrentEnvironment({
25
+ config
26
+ })?.url;
27
+ if (!type.isNone(url)) {
28
+ process.env.AUTH_URL = url;
29
+ }
30
+ }
31
+ export default defaultAuthUrl;
@@ -19,10 +19,15 @@ 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 defaultAuthUrl from './defaultAuthUrl.js';
23
+ import resolveCookies from './resolveCookies.js';
24
+ const authConfigCache = {};
23
25
  let initialized = false;
24
- function getNextAuthConfig({ appMeta, authJson, logger, plugins, secrets }) {
25
- if (initialized) return nextAuthConfig;
26
+ function getAuthConfig({ appMeta, authJson, config, dev, logger, plugins, secrets }) {
27
+ if (initialized) return authConfigCache;
28
+ defaultAuthUrl({
29
+ config
30
+ });
26
31
  const operatorsParser = new ServerParser({
27
32
  lowdefyApp: appMeta,
28
33
  operators: {
@@ -40,36 +45,46 @@ function getNextAuthConfig({ appMeta, authJson, logger, plugins, secrets }) {
40
45
  if (operatorErrors.length > 0) {
41
46
  throw operatorErrors[0];
42
47
  }
43
- nextAuthConfig.adapter = createAdapter({
48
+ authConfigCache.adapter = createAdapter({
44
49
  authConfig,
45
50
  logger,
46
51
  plugins
47
52
  });
48
- nextAuthConfig.callbacks = createCallbacks({
53
+ authConfigCache.callbacks = createCallbacks({
49
54
  authConfig,
50
55
  logger,
51
56
  plugins
52
57
  });
53
- nextAuthConfig.events = createEvents({
58
+ authConfigCache.events = createEvents({
54
59
  authConfig,
55
60
  logger,
56
61
  plugins
57
62
  });
58
- nextAuthConfig.logger = createLogger({
63
+ authConfigCache.logger = createLogger({
59
64
  logger
60
65
  });
61
- nextAuthConfig.providers = createProviders({
66
+ authConfigCache.providers = createProviders({
62
67
  authConfig,
63
68
  logger,
64
69
  plugins
65
70
  });
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;
71
+ authConfigCache.debug = authConfig.debug ?? logger?.isLevelEnabled('debug') === true;
72
+ authConfigCache.pages = authConfig.authPages;
73
+ authConfigCache.session = authConfig.session;
74
+ authConfigCache.theme = authConfig.theme;
75
+ authConfigCache.cookies = resolveCookies({
76
+ appMeta,
77
+ authConfig,
78
+ dev
79
+ });
80
+ // Auth.js v5 reads AUTH_SECRET; the v4 NEXTAUTH_SECRET variable is not
81
+ // supported — deployments must rename it (see the v5-to-v6 migration doc).
82
+ authConfigCache.secret = process.env.AUTH_SECRET;
83
+ // Self-hosted servers run behind arbitrary proxies; derive URLs from request
84
+ // headers (v4 derived them from NEXTAUTH_URL, aliased to AUTH_URL at startup).
85
+ authConfigCache.trustHost = true;
86
+ authConfigCache.basePath = '/api/auth';
72
87
  initialized = true;
73
- return nextAuthConfig;
88
+ return authConfigCache;
74
89
  }
75
- export default getNextAuthConfig;
90
+ 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,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,12 +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
18
  import buildEndpointResult from '../../response/buildEndpointResult.js';
19
19
  import createEvaluateOperators from '../../context/createEvaluateOperators.js';
20
20
  import getEndpointConfig from './getEndpointConfig.js';
21
+ import isUnauthenticatedHuman from './isUnauthenticatedHuman.js';
21
22
  import runRoutine from './runRoutine.js';
23
+ import scheduleBackground from './scheduleBackground.js';
22
24
  async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
23
25
  const { logger } = context;
24
26
  context.blockId = blockId;
@@ -35,9 +37,11 @@ async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
35
37
  const endpointConfig = await getEndpointConfig(context, {
36
38
  endpointId
37
39
  });
38
- // 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.
39
43
  if (endpointConfig.type === 'InternalApi') {
40
- 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.`);
41
45
  logger.debug({
42
46
  params: {
43
47
  endpointId
@@ -53,10 +57,30 @@ async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
53
57
  steps: {},
54
58
  payload: serializer.deserialize(payload),
55
59
  arrayIndices: [],
60
+ error: null,
56
61
  items: {},
57
62
  state: {},
58
63
  endpointDepth: 0
59
64
  };
65
+ // async: true — acknowledge now, run the routine in the background.
66
+ // Auth was already checked above; the outcome lands in logs (scheduleBackground)
67
+ // and in whatever the routine itself records.
68
+ if (endpointConfig.async === true) {
69
+ scheduleBackground(context, {
70
+ event: 'background_endpoint',
71
+ endpointId
72
+ }, ()=>runRoutine(context, routineContext, {
73
+ routine: endpointConfig.routine
74
+ }));
75
+ return {
76
+ error: null,
77
+ response: serializer.serialize({
78
+ accepted: true
79
+ }),
80
+ status: 'accepted',
81
+ success: true
82
+ };
83
+ }
60
84
  const { error, response, status } = await runRoutine(context, routineContext, {
61
85
  routine: endpointConfig.routine
62
86
  });
@@ -14,20 +14,17 @@
14
14
  limitations under the License.
15
15
  */ import { ConfigError } from '@lowdefy/errors';
16
16
  import runRoutine from '../runRoutine.js';
17
+ import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
17
18
  async function controlFor(context, routineContext, { control }) {
18
- const { endpointId, logger, evaluateOperators } = context;
19
+ const { endpointId, logger } = context;
19
20
  const { items } = routineContext;
20
21
  const itemName = control[':for'];
21
22
  if (!itemName) {
22
23
  throw new Error(`Invalid :for in endpoint "${endpointId}" - missing variable name in :for.`);
23
24
  }
24
- const array = evaluateOperators({
25
+ const array = evaluateRoutineOperators(context, routineContext, {
25
26
  input: control[':in'],
26
- items,
27
- location: control['~k'] ?? ':for',
28
- payload: routineContext.payload,
29
- state: routineContext.state,
30
- steps: routineContext.steps
27
+ location: control['~k'] ?? ':for'
31
28
  });
32
29
  logger.debug({
33
30
  event: 'debug_control_for',
@@ -13,16 +13,12 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import runRoutine from '../runRoutine.js';
16
+ import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
16
17
  async function controlIf(context, routineContext, { control }) {
17
- const { endpointId, logger, evaluateOperators } = context;
18
- const { items } = routineContext;
19
- const evaluatedIf = evaluateOperators({
18
+ const { endpointId, logger } = context;
19
+ const evaluatedIf = evaluateRoutineOperators(context, routineContext, {
20
20
  input: control[':if'],
21
- items,
22
- location: control['~k'] ?? ':if',
23
- payload: routineContext.payload,
24
- state: routineContext.state,
25
- steps: routineContext.steps
21
+ location: control['~k'] ?? ':if'
26
22
  });
27
23
  logger.debug({
28
24
  event: 'debug_control_if',